@nklisch/pi-agile-workflow 0.15.3

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 (203) hide show
  1. package/.claude-plugin/plugin.json +10 -0
  2. package/.codex-plugin/plugin.json +28 -0
  3. package/CHANGELOG.md +591 -0
  4. package/README.md +107 -0
  5. package/docs/ARCHITECTURE.md +716 -0
  6. package/docs/MIGRATION.md +419 -0
  7. package/docs/PRINCIPLES.md +464 -0
  8. package/docs/ROADMAP.md +95 -0
  9. package/docs/SPEC.md +855 -0
  10. package/docs/VISION.md +106 -0
  11. package/extensions/agile-workflow.test.ts +419 -0
  12. package/extensions/agile-workflow.ts +467 -0
  13. package/hooks/hooks.json +52 -0
  14. package/hooks/scripts/prompt-context.py +628 -0
  15. package/hooks/scripts/substrate-maintainer.py +359 -0
  16. package/hooks/scripts/test_prompt_context.py +757 -0
  17. package/package.json +30 -0
  18. package/scripts/install-work-view.sh +165 -0
  19. package/scripts/tests/agent-metadata.test.sh +75 -0
  20. package/scripts/tests/bump-version.test.sh +552 -0
  21. package/scripts/tests/channel-parity.test.sh +117 -0
  22. package/scripts/tests/convert-content-integrity.test.sh +246 -0
  23. package/scripts/tests/convert-install-routing.test.sh +159 -0
  24. package/scripts/tests/install-work-view.test.sh +411 -0
  25. package/scripts/tests/pi-package-metadata.test.sh +119 -0
  26. package/scripts/tests/work-board-shim.test.sh +239 -0
  27. package/scripts/tests/work-view-dist-version.test.sh +105 -0
  28. package/scripts/work-board.sh +64 -0
  29. package/scripts/work-view.sh +423 -0
  30. package/skills/autopilot/SKILL.md +330 -0
  31. package/skills/autopilot/agents/openai.yaml +6 -0
  32. package/skills/board/SKILL.md +62 -0
  33. package/skills/board/agents/openai.yaml +6 -0
  34. package/skills/bold-refactor/SKILL.md +244 -0
  35. package/skills/bold-refactor/agents/openai.yaml +6 -0
  36. package/skills/bug-scan/SKILL.md +378 -0
  37. package/skills/bug-scan/agents/openai.yaml +6 -0
  38. package/skills/bug-scan/references/async-promises.md +119 -0
  39. package/skills/bug-scan/references/concurrency-races.md +148 -0
  40. package/skills/bug-scan/references/data-layer.md +167 -0
  41. package/skills/bug-scan/references/error-handling.md +197 -0
  42. package/skills/bug-scan/references/gate-item-template.md +94 -0
  43. package/skills/bug-scan/references/language-footguns.md +219 -0
  44. package/skills/bug-scan/references/parked-item-template.md +103 -0
  45. package/skills/bug-scan/references/report-template.md +115 -0
  46. package/skills/bug-scan/references/resource-leaks.md +199 -0
  47. package/skills/bug-scan/references/state-closures.md +148 -0
  48. package/skills/bug-scan/references/time-numbers.md +158 -0
  49. package/skills/convert/SKILL.md +1463 -0
  50. package/skills/convert/agents/openai.yaml +6 -0
  51. package/skills/convert/references/legacy-overlap-migration.md +179 -0
  52. package/skills/deep-code-scan/SKILL.md +378 -0
  53. package/skills/deep-code-scan/agents/openai.yaml +6 -0
  54. package/skills/deep-code-scan/references/artifact-ledger.md +148 -0
  55. package/skills/deep-code-scan/references/consolidation.md +91 -0
  56. package/skills/deep-code-scan/references/decomposition.md +127 -0
  57. package/skills/deep-code-scan/references/item-templates.md +263 -0
  58. package/skills/deep-code-scan/references/lane-catalog.md +129 -0
  59. package/skills/deep-code-scan/references/review-gauntlet.md +108 -0
  60. package/skills/deep-code-scan/references/scanner-brief.md +152 -0
  61. package/skills/e2e-test-design/SKILL.md +517 -0
  62. package/skills/e2e-test-design/references/anti-tautology.md +261 -0
  63. package/skills/e2e-test-design/references/service-mocks.md +234 -0
  64. package/skills/e2e-test-design/references/test-taxonomy.md +211 -0
  65. package/skills/epic-design/SKILL.md +476 -0
  66. package/skills/epicize/SKILL.md +210 -0
  67. package/skills/epicize/agents/openai.yaml +6 -0
  68. package/skills/feature-design/SKILL.md +472 -0
  69. package/skills/fix/SKILL.md +173 -0
  70. package/skills/gate-cruft/SKILL.md +258 -0
  71. package/skills/gate-docs/SKILL.md +278 -0
  72. package/skills/gate-patterns/SKILL.md +389 -0
  73. package/skills/gate-refactor/SKILL.md +408 -0
  74. package/skills/gate-security/SKILL.md +275 -0
  75. package/skills/gate-tests/SKILL.md +367 -0
  76. package/skills/groom/SKILL.md +162 -0
  77. package/skills/groom/agents/openai.yaml +6 -0
  78. package/skills/ideate/SKILL.md +185 -0
  79. package/skills/ideate/agents/openai.yaml +6 -0
  80. package/skills/implement/SKILL.md +288 -0
  81. package/skills/implement-orchestrator/SKILL.md +604 -0
  82. package/skills/park/SKILL.md +111 -0
  83. package/skills/perf-design/SKILL.md +427 -0
  84. package/skills/perf-scout/SKILL.md +404 -0
  85. package/skills/perf-scout/agents/openai.yaml +6 -0
  86. package/skills/perf-scout/references/algorithmic-and-data-structures.md +126 -0
  87. package/skills/perf-scout/references/approximation-and-precomputation.md +136 -0
  88. package/skills/perf-scout/references/caching-and-memoization.md +123 -0
  89. package/skills/perf-scout/references/compiler-runtime-and-language.md +136 -0
  90. package/skills/perf-scout/references/database-and-storage-internals.md +144 -0
  91. package/skills/perf-scout/references/distributed-systems.md +150 -0
  92. package/skills/perf-scout/references/game-engine-and-realtime.md +136 -0
  93. package/skills/perf-scout/references/gpu-and-accelerators.md +130 -0
  94. package/skills/perf-scout/references/idea-ranking.md +75 -0
  95. package/skills/perf-scout/references/io-and-batching.md +135 -0
  96. package/skills/perf-scout/references/memory-and-data-locality.md +137 -0
  97. package/skills/perf-scout/references/parallelism-and-vectorization.md +126 -0
  98. package/skills/perf-scout/references/parked-item-template.md +117 -0
  99. package/skills/perf-scout/references/peer-review-pass.md +150 -0
  100. package/skills/perf-scout/references/report-template.md +148 -0
  101. package/skills/principles/SKILL.md +678 -0
  102. package/skills/principles/references/models.md +191 -0
  103. package/skills/principles/references/subagents.md +158 -0
  104. package/skills/prose-author/SKILL.md +185 -0
  105. package/skills/refactor-conventions-creator/SKILL.md +359 -0
  106. package/skills/refactor-conventions-creator/agents/openai.yaml +6 -0
  107. package/skills/refactor-conventions-creator/references/common-structures.md +91 -0
  108. package/skills/refactor-conventions-creator/references/common-styles.md +90 -0
  109. package/skills/refactor-design/SKILL.md +384 -0
  110. package/skills/release-deploy/SKILL.md +667 -0
  111. package/skills/release-deploy/agents/openai.yaml +6 -0
  112. package/skills/research/SKILL.md +166 -0
  113. package/skills/research/agents/openai.yaml +6 -0
  114. package/skills/review/SKILL.md +267 -0
  115. package/skills/review/references/deep-review.md +98 -0
  116. package/skills/review/references/review-lenses.md +68 -0
  117. package/skills/review/references/substrate-side-effects.md +161 -0
  118. package/skills/review/references/target-resolution.md +52 -0
  119. package/skills/scope/SKILL.md +486 -0
  120. package/work-view/Cargo.lock +486 -0
  121. package/work-view/Cargo.toml +9 -0
  122. package/work-view/crates/cli/.work-view-version +1 -0
  123. package/work-view/crates/cli/Cargo.toml +18 -0
  124. package/work-view/crates/cli/src/actionable.rs +500 -0
  125. package/work-view/crates/cli/src/args.rs +792 -0
  126. package/work-view/crates/cli/src/board/assets/board.css +855 -0
  127. package/work-view/crates/cli/src/board/assets/board.js +259 -0
  128. package/work-view/crates/cli/src/board/assets/card.js +139 -0
  129. package/work-view/crates/cli/src/board/assets/components.css +290 -0
  130. package/work-view/crates/cli/src/board/assets/dependency.js +1193 -0
  131. package/work-view/crates/cli/src/board/assets/detail.js +256 -0
  132. package/work-view/crates/cli/src/board/assets/filters.js +389 -0
  133. package/work-view/crates/cli/src/board/assets/index.html +77 -0
  134. package/work-view/crates/cli/src/board/assets/kanban.js +231 -0
  135. package/work-view/crates/cli/src/board/assets/markdown.js +260 -0
  136. package/work-view/crates/cli/src/board/assets/motion.css +131 -0
  137. package/work-view/crates/cli/src/board/assets/state.js +245 -0
  138. package/work-view/crates/cli/src/board/assets/table.js +304 -0
  139. package/work-view/crates/cli/src/board/assets/tokens.css +602 -0
  140. package/work-view/crates/cli/src/board/assets/views.js +41 -0
  141. package/work-view/crates/cli/src/board/assets.rs +162 -0
  142. package/work-view/crates/cli/src/board/feed.rs +204 -0
  143. package/work-view/crates/cli/src/board/mod.rs +246 -0
  144. package/work-view/crates/cli/src/board/open.rs +144 -0
  145. package/work-view/crates/cli/src/board/server.rs +407 -0
  146. package/work-view/crates/cli/src/main.rs +195 -0
  147. package/work-view/crates/cli/src/render.rs +354 -0
  148. package/work-view/crates/cli/src/scope.rs +157 -0
  149. package/work-view/crates/cli/src/stale.rs +581 -0
  150. package/work-view/crates/cli/tests/board-js/dependency-table.test.mjs +64 -0
  151. package/work-view/crates/cli/tests/board-js/expanded-browsing.test.mjs +79 -0
  152. package/work-view/crates/cli/tests/board-js/harness-smoke.test.mjs +11 -0
  153. package/work-view/crates/cli/tests/board-js/harness.mjs +437 -0
  154. package/work-view/crates/cli/tests/board-js/kanban-detail.test.mjs +75 -0
  155. package/work-view/crates/cli/tests/board-js/markdown-filter.test.mjs +60 -0
  156. package/work-view/crates/cli/tests/fixtures/golden/.work/CONVENTIONS.md +3 -0
  157. package/work-view/crates/cli/tests/fixtures/golden/.work/active/epics/epic-alpha.md +16 -0
  158. package/work-view/crates/cli/tests/fixtures/golden/.work/active/features/feat-a.md +16 -0
  159. package/work-view/crates/cli/tests/fixtures/golden/.work/active/features/feat-b.md +16 -0
  160. package/work-view/crates/cli/tests/fixtures/golden/.work/active/stories/story-alpha-1.md +16 -0
  161. package/work-view/crates/cli/tests/fixtures/golden/.work/active/stories/story-research-1.md +19 -0
  162. package/work-view/crates/cli/tests/fixtures/golden/.work/archive/feat-done.md +16 -0
  163. package/work-view/crates/cli/tests/fixtures/golden/.work/archive/feat-shipped.md +18 -0
  164. package/work-view/crates/cli/tests/fixtures/golden/.work/backlog/idea-backlog.md +7 -0
  165. package/work-view/crates/cli/tests/fixtures/golden/.work/releases/v1.0/v1.0.md +16 -0
  166. package/work-view/crates/cli/tests/fixtures/ready-drafting/.work/CONVENTIONS.md +6 -0
  167. package/work-view/crates/cli/tests/fixtures/ready-drafting/.work/active/features/feat-dep-done.md +17 -0
  168. package/work-view/crates/cli/tests/fixtures/ready-drafting/.work/active/features/feat-design-ready.md +17 -0
  169. package/work-view/crates/cli/tests/fixtures/ready-drafting/.work/active/features/feat-impl-ready.md +17 -0
  170. package/work-view/crates/cli/tests/fixtures/ready-drafting/.work/active/stories/story-review-ready.md +17 -0
  171. package/work-view/crates/cli/tests/fixtures/with-malformed/.work/CONVENTIONS.md +4 -0
  172. package/work-view/crates/cli/tests/fixtures/with-malformed/.work/active/epics/malformed-no-closing-fence.md +8 -0
  173. package/work-view/crates/cli/tests/fixtures/with-malformed/.work/active/features/good-item.md +16 -0
  174. package/work-view/crates/cli/tests/integration.rs +2833 -0
  175. package/work-view/crates/core/Cargo.toml +13 -0
  176. package/work-view/crates/core/src/error.rs +61 -0
  177. package/work-view/crates/core/src/filter.rs +804 -0
  178. package/work-view/crates/core/src/graph.rs +324 -0
  179. package/work-view/crates/core/src/index.rs +590 -0
  180. package/work-view/crates/core/src/lib.rs +54 -0
  181. package/work-view/crates/core/src/model.rs +202 -0
  182. package/work-view/crates/core/src/parse.rs +494 -0
  183. package/work-view/crates/core/tests/fixtures/golden/.work/CONVENTIONS.md +15 -0
  184. package/work-view/crates/core/tests/fixtures/golden/.work/active/epics/epic-core.md +16 -0
  185. package/work-view/crates/core/tests/fixtures/golden/.work/active/features/feat-parser.md +16 -0
  186. package/work-view/crates/core/tests/fixtures/golden/.work/active/features/feat-query.md +16 -0
  187. package/work-view/crates/core/tests/fixtures/golden/.work/active/stories/malformed-no-id.md +15 -0
  188. package/work-view/crates/core/tests/fixtures/golden/.work/active/stories/story-parse-unit.md +16 -0
  189. package/work-view/crates/core/tests/fixtures/golden/.work/active/stories/story-security-gate.md +16 -0
  190. package/work-view/crates/core/tests/fixtures/golden/.work/archive/feat-old.md +16 -0
  191. package/work-view/crates/core/tests/fixtures/golden/.work/archive/feat-parser.md +17 -0
  192. package/work-view/crates/core/tests/fixtures/golden/.work/backlog/idea-future-board.md +7 -0
  193. package/work-view/crates/core/tests/fixtures/golden/.work/releases/v1.0/v1.0.md +16 -0
  194. package/work-view/crates/core/tests/fixtures/precedence/.work/CONVENTIONS.md +10 -0
  195. package/work-view/crates/core/tests/fixtures/precedence/.work/archive/dup-item.md +18 -0
  196. package/work-view/crates/core/tests/fixtures/precedence/.work/releases/v2.0/dup-item.md +18 -0
  197. package/work-view/crates/core/tests/integration.rs +541 -0
  198. package/work-view/dist/.gitattributes +1 -0
  199. package/work-view/dist/README.md +42 -0
  200. package/work-view/dist/aarch64-apple-darwin/work-view +0 -0
  201. package/work-view/dist/aarch64-unknown-linux-musl/work-view +0 -0
  202. package/work-view/dist/x86_64-apple-darwin/work-view +0 -0
  203. package/work-view/dist/x86_64-unknown-linux-musl/work-view +0 -0
@@ -0,0 +1,259 @@
1
+ import { createBoardStore } from "/assets/state.js";
2
+ import { openDetail, closeDetail, syncDetail } from "/assets/detail.js";
3
+ import { renderCard } from "/assets/card.js";
4
+ import { renderFilterBar } from "/assets/filters.js";
5
+ import { mountCurrentView } from "/assets/views.js";
6
+
7
+ const root = document.documentElement;
8
+ const accentPicker = document.getElementById("theme-accent");
9
+ const modeButtons = Array.from(document.querySelectorAll("[data-mode]"));
10
+ const viewTabs = Array.from(document.querySelectorAll("[data-view]"));
11
+ const statusRegion = document.getElementById("status-region");
12
+ const statusText = statusRegion?.querySelector(".status-text");
13
+ const statusSpinner = statusRegion?.querySelector(".spinner");
14
+ const diagnosticsSummary = document.getElementById("diagnostics-summary");
15
+ const diagnosticsRegion = document.getElementById("diagnostics-region");
16
+ const viewRoot = document.getElementById("view-root");
17
+ const refreshButton = document.getElementById("refresh-button");
18
+ const filterRoot = document.getElementById("global-filter-container");
19
+
20
+ const store = createBoardStore({ storage: window.localStorage, root: document });
21
+ const context = {
22
+ ...store,
23
+ renderCard: (item, options = {}) => renderCard(item, { ...options, context }),
24
+ openDetail: (id) => openDetail(id, context),
25
+ closeDetail: () => closeDetail(context),
26
+ };
27
+ const filtersUi = filterRoot ? renderFilterBar(filterRoot, context) : null;
28
+ window.boardContext = context;
29
+
30
+ let mountedSnapshot = null;
31
+ let mountedView = null;
32
+ let mountedFilters = "";
33
+
34
+ function replaceChildren(node, children) {
35
+ if (!node) {
36
+ return;
37
+ }
38
+ node.replaceChildren(...children);
39
+ }
40
+
41
+ function textElement(tag, className, text) {
42
+ const element = document.createElement(tag);
43
+ if (className) {
44
+ element.className = className;
45
+ }
46
+ element.textContent = text;
47
+ return element;
48
+ }
49
+
50
+ function emptyState(glyph, title, body) {
51
+ const wrapper = document.createElement("div");
52
+ wrapper.className = "view-placeholder empty";
53
+ wrapper.append(
54
+ textElement("span", "glyph", glyph),
55
+ textElement("h1", "", title),
56
+ textElement("p", "", body),
57
+ );
58
+ return wrapper;
59
+ }
60
+
61
+ function alertNode(kind, title, detail) {
62
+ const alert = document.createElement("div");
63
+ alert.className = `alert alert--${kind}`;
64
+ const icon = textElement("span", "ico", kind === "danger" ? "!" : kind === "warning" ? "?" : "i");
65
+ icon.setAttribute("aria-hidden", "true");
66
+ const body = document.createElement("span");
67
+ body.className = "body";
68
+ body.append(textElement("strong", "", title));
69
+ if (detail) {
70
+ body.append(textElement("small", "", detail));
71
+ }
72
+ alert.append(icon, body);
73
+ return alert;
74
+ }
75
+
76
+ function diagnosticsFor(snapshot) {
77
+ const diagnostics = snapshot?.diagnostics || {};
78
+ const entries = [];
79
+ for (const error of diagnostics.parse_errors || []) {
80
+ entries.push({
81
+ kind: "danger",
82
+ title: "Parse error",
83
+ detail: [error.rel_path, error.reason].filter(Boolean).join(" - "),
84
+ });
85
+ }
86
+ for (const warning of diagnostics.validation_warnings || []) {
87
+ entries.push({
88
+ kind: "warning",
89
+ title: warning.id ? `Validation warning: ${warning.id}` : "Validation warning",
90
+ detail: [warning.rel_path, warning.field, warning.reason].filter(Boolean).join(" - "),
91
+ });
92
+ }
93
+ for (const duplicate of diagnostics.duplicate_ids || []) {
94
+ entries.push({
95
+ kind: "warning",
96
+ title: duplicate.id ? `Duplicate id: ${duplicate.id}` : "Duplicate id",
97
+ detail: [duplicate.rel_path, duplicate.reason].filter(Boolean).join(" - "),
98
+ });
99
+ }
100
+ for (const id of diagnostics.client_duplicate_ids || []) {
101
+ entries.push({
102
+ kind: "warning",
103
+ title: `Duplicate id in feed: ${id}`,
104
+ detail: "The browser snapshot contains more than one item with this id.",
105
+ });
106
+ }
107
+ return entries;
108
+ }
109
+
110
+ function applyTheme(theme) {
111
+ root.dataset.accent = theme.accent;
112
+ if (theme.mode === "system") {
113
+ root.removeAttribute("data-theme");
114
+ } else {
115
+ root.dataset.theme = theme.mode;
116
+ }
117
+ if (accentPicker) {
118
+ accentPicker.value = theme.accent;
119
+ }
120
+ for (const button of modeButtons) {
121
+ button.setAttribute("aria-pressed", String(button.dataset.mode === theme.mode));
122
+ }
123
+ }
124
+
125
+ function applyView(view) {
126
+ for (const tab of viewTabs) {
127
+ tab.setAttribute("aria-selected", String(tab.dataset.view === view));
128
+ }
129
+ }
130
+
131
+ function updateStatus(state, visibleCount) {
132
+ if (statusSpinner) {
133
+ statusSpinner.hidden = !state.loading;
134
+ }
135
+ if (refreshButton) {
136
+ refreshButton.disabled = state.loading;
137
+ }
138
+ if (!statusText) {
139
+ return;
140
+ }
141
+ if (state.loading && !state.snapshot) {
142
+ statusText.textContent = "Loading substrate data";
143
+ return;
144
+ }
145
+ if (state.error && !state.snapshot) {
146
+ statusText.textContent = "Substrate feed unavailable";
147
+ return;
148
+ }
149
+ if (state.snapshot) {
150
+ const total = state.snapshot.items.length;
151
+ const project = state.snapshot.project || "substrate";
152
+ const version = state.snapshot.work_view_version || "unknown";
153
+ statusText.textContent = `${project} / ${version} / ${visibleCount}/${total} visible`;
154
+ return;
155
+ }
156
+ statusText.textContent = "Waiting for substrate data";
157
+ }
158
+
159
+ function filterSignature(filters) {
160
+ const setValues = (set) => Array.from(set).sort().join("\u0000");
161
+ return [
162
+ filters.search,
163
+ setValues(filters.kinds),
164
+ setValues(filters.stages),
165
+ setValues(filters.parents),
166
+ setValues(filters.tags),
167
+ String(filters.autoHideReleased),
168
+ ].join("\u0001");
169
+ }
170
+
171
+ function resetMountedView() {
172
+ mountedSnapshot = null;
173
+ mountedView = null;
174
+ mountedFilters = "";
175
+ }
176
+
177
+ function renderDiagnostics(state) {
178
+ const diagnostics = diagnosticsFor(state.snapshot);
179
+ diagnosticsSummary?.replaceChildren(textElement("span", "count", `${diagnostics.length} diagnostics`));
180
+ const nodes = diagnostics.map((entry) => alertNode(entry.kind, entry.title, entry.detail));
181
+ if (state.error) {
182
+ nodes.unshift(alertNode("danger", "Feed request failed", state.error));
183
+ }
184
+ replaceChildren(diagnosticsRegion, nodes);
185
+ }
186
+
187
+ function renderView(state) {
188
+ if (!viewRoot) {
189
+ return;
190
+ }
191
+ if (state.loading && !state.snapshot) {
192
+ resetMountedView();
193
+ viewRoot.replaceChildren(emptyState("::", "Loading substrate data", "Fetching /api/substrate from the local board host."));
194
+ return;
195
+ }
196
+ if (state.error && !state.snapshot) {
197
+ resetMountedView();
198
+ viewRoot.replaceChildren(emptyState("!", "Feed unavailable", "The board shell is still usable; refresh when the substrate feed is available."));
199
+ return;
200
+ }
201
+ if (!state.snapshot) {
202
+ resetMountedView();
203
+ viewRoot.replaceChildren(emptyState("::", "Board shell ready", "Kanban, dependency, and table views mount here as their stories land."));
204
+ return;
205
+ }
206
+
207
+ const visible = context.visibleItems();
208
+ if (state.snapshot.items.length === 0) {
209
+ resetMountedView();
210
+ viewRoot.replaceChildren(emptyState("0", "No substrate items", "The feed loaded successfully, but it did not contain any items."));
211
+ return;
212
+ }
213
+ if (visible.length === 0) {
214
+ resetMountedView();
215
+ viewRoot.replaceChildren(emptyState("0", "No matching items", "Adjust filters or show released and archived items to widen the view."));
216
+ return;
217
+ }
218
+
219
+ const filters = filterSignature(state.filters);
220
+ // Detail selection intentionally does not remount views; otherwise opening a
221
+ // card destroys the trigger element before focus can be restored on close.
222
+ if (mountedSnapshot !== state.snapshot || mountedView !== state.view || mountedFilters !== filters) {
223
+ mountedSnapshot = state.snapshot;
224
+ mountedView = state.view;
225
+ mountedFilters = filters;
226
+ mountCurrentView(viewRoot, context);
227
+ }
228
+ }
229
+
230
+ function render(state) {
231
+ const visibleCount = context.visibleItems().length;
232
+ applyTheme(state.theme);
233
+ applyView(state.view);
234
+ filtersUi?.sync(state);
235
+ updateStatus(state, visibleCount);
236
+ renderDiagnostics(state);
237
+ renderView(state);
238
+ syncDetail(context);
239
+ }
240
+
241
+ accentPicker?.addEventListener("change", (event) => {
242
+ context.setTheme({ accent: event.target.value });
243
+ });
244
+
245
+ for (const button of modeButtons) {
246
+ button.addEventListener("click", () => context.setTheme({ mode: button.dataset.mode || "system" }));
247
+ }
248
+
249
+ for (const tab of viewTabs) {
250
+ tab.addEventListener("click", () => context.setView(tab.dataset.view || "kanban"));
251
+ }
252
+
253
+ refreshButton?.addEventListener("click", () => {
254
+ void context.refresh();
255
+ });
256
+
257
+ context.subscribe(render);
258
+ render(context.getState());
259
+ void context.refresh();
@@ -0,0 +1,139 @@
1
+ import { markdownSummary, renderInlineMarkdown } from "/assets/markdown.js";
2
+
3
+ function text(tag, className, value) {
4
+ const node = document.createElement(tag);
5
+ if (className) {
6
+ node.className = className;
7
+ }
8
+ node.textContent = value;
9
+ return node;
10
+ }
11
+
12
+ function valueOrNone(value) {
13
+ return value == null || value === "" ? "—" : String(value);
14
+ }
15
+
16
+ function countOf(value) {
17
+ return Array.isArray(value) ? value.length : 0;
18
+ }
19
+
20
+ function kindClass(kind) {
21
+ const normalized = valueOrNone(kind).toLowerCase();
22
+ return ["epic", "feature", "story", "release", "backlog"].includes(normalized)
23
+ ? normalized
24
+ : "backlog";
25
+ }
26
+
27
+ function badge(label, className) {
28
+ return text("span", `badge ${className}`, label);
29
+ }
30
+
31
+ function meta(label, value, className = "") {
32
+ const node = text("span", className, `${label}: ${valueOrNone(value)}`);
33
+ node.dataset.label = label;
34
+ return node;
35
+ }
36
+
37
+ function dependencyMeta(item) {
38
+ const deps = countOf(item.depends_on);
39
+ const unmet = countOf(item.unmet_deps);
40
+ const dependents = countOf(item.dependents);
41
+ const children = countOf(item.children);
42
+ const node = document.createElement("span");
43
+ node.className = "dep";
44
+ node.append(
45
+ text("span", "arrow", "deps"),
46
+ document.createTextNode(` ${deps}`),
47
+ document.createTextNode(` / unmet ${unmet}`),
48
+ document.createTextNode(` / unblocks ${dependents}`),
49
+ document.createTextNode(` / children ${children}`),
50
+ );
51
+ return node;
52
+ }
53
+
54
+ function resolveOpen(options) {
55
+ if (typeof options.onOpen === "function") {
56
+ return options.onOpen;
57
+ }
58
+ const context = options.context || options.ctx;
59
+ if (context && typeof context.openDetail === "function") {
60
+ return (id) => context.openDetail(id);
61
+ }
62
+ return null;
63
+ }
64
+
65
+ function activateCard(item, options) {
66
+ const open = resolveOpen(options);
67
+ if (open && item?.id) {
68
+ open(item.id);
69
+ }
70
+ }
71
+
72
+ export function renderCard(item, options = {}) {
73
+ const kind = kindClass(item?.kind);
74
+ const card = document.createElement("article");
75
+ card.className = [
76
+ "item-card",
77
+ `item-card--${kind}`,
78
+ options.compact ? "item-card--compact" : "",
79
+ item?.blocked ? "is-blocked" : "",
80
+ options.selected ? "is-selected" : "",
81
+ ].filter(Boolean).join(" ");
82
+ card.tabIndex = 0;
83
+ card.setAttribute("role", "button");
84
+ card.setAttribute("aria-label", `Open ${valueOrNone(item?.id)}`);
85
+ if (item?.id) {
86
+ card.dataset.id = String(item.id);
87
+ }
88
+
89
+ const head = document.createElement("div");
90
+ head.className = "ic-head";
91
+ head.append(
92
+ text("span", `chip chip--${kind}`, valueOrNone(item?.kind)),
93
+ text("span", "ic-id", valueOrNone(item?.id)),
94
+ );
95
+ if (item?.ready) {
96
+ head.append(badge("ready", "badge--ready"));
97
+ }
98
+ if (item?.blocked) {
99
+ head.append(badge("blocked", "badge--blocked"));
100
+ }
101
+ if (item?.is_terminal || item?.stage === "done" || item?.stage === "released") {
102
+ head.append(badge("done", "badge--done"));
103
+ }
104
+
105
+ const summary = document.createElement("div");
106
+ summary.className = "ic-body";
107
+ const summaryText = markdownSummary(item?.body || "", options.compact ? 120 : 220);
108
+ if (summaryText) {
109
+ summary.append(renderInlineMarkdown(summaryText, { allowLinks: false }));
110
+ } else {
111
+ summary.textContent = "No body summary.";
112
+ summary.classList.add("is-empty");
113
+ }
114
+
115
+ const metaRow = document.createElement("div");
116
+ metaRow.className = "ic-meta";
117
+ metaRow.append(
118
+ meta("stage", item?.stage, "ic-stage"),
119
+ meta("parent", item?.parent, "ic-parent"),
120
+ dependencyMeta(item || {}),
121
+ );
122
+
123
+ const tags = Array.isArray(item?.tags) ? item.tags : [];
124
+ if (tags.length > 0) {
125
+ metaRow.append(meta("tags", tags.join(", "), "ic-tags"));
126
+ }
127
+
128
+ card.append(head, summary, metaRow);
129
+
130
+ card.addEventListener("click", () => activateCard(item, options));
131
+ card.addEventListener("keydown", (event) => {
132
+ if (event.key === "Enter" || event.key === " ") {
133
+ event.preventDefault();
134
+ activateCard(item, options);
135
+ }
136
+ });
137
+
138
+ return card;
139
+ }