@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,77 @@
1
+ <!doctype html>
2
+ <html lang="en" data-accent="teal">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>agile-workflow board</title>
7
+ <link rel="stylesheet" href="/assets/tokens.css">
8
+ <link rel="stylesheet" href="/assets/components.css">
9
+ <link rel="stylesheet" href="/assets/motion.css">
10
+ <link rel="stylesheet" href="/assets/board.css">
11
+ <script type="module" src="/assets/board.js"></script>
12
+ </head>
13
+ <body>
14
+ <div class="app-shell u-theme-anim">
15
+ <header class="appbar" aria-label="Board application bar">
16
+ <div class="brand" aria-label="agile-workflow substrate board">
17
+ <span>substrate</span><span class="mark">/</span><span>board</span>
18
+ </div>
19
+
20
+ <nav class="tabs view-tabs" aria-label="Board views">
21
+ <button class="tab" type="button" data-view="kanban" aria-selected="true">Kanban</button>
22
+ <button class="tab" type="button" data-view="dependency" aria-selected="false">Dependency</button>
23
+ <button class="tab" type="button" data-view="table" aria-selected="false">Table</button>
24
+ </nav>
25
+
26
+ <div class="appbar-actions">
27
+ <div class="theme-picker" aria-label="Theme picker">
28
+ <label class="lbl" for="theme-accent">accent</label>
29
+ <select class="select" id="theme-accent" name="accent">
30
+ <option value="teal" selected>teal</option>
31
+ <option value="amber">amber</option>
32
+ <option value="violet">violet</option>
33
+ <option value="azure">azure</option>
34
+ <option value="lime">lime</option>
35
+ <option value="candy">candy</option>
36
+ </select>
37
+ <span class="seg" id="theme-mode" aria-label="Color mode">
38
+ <button class="seg-btn" type="button" data-mode="system" aria-pressed="true">Sys</button>
39
+ <button class="seg-btn" type="button" data-mode="light" aria-pressed="false">Light</button>
40
+ <button class="seg-btn" type="button" data-mode="dark" aria-pressed="false">Dark</button>
41
+ </span>
42
+ </div>
43
+ <span class="status-line" id="status-region" role="status" aria-live="polite">
44
+ <span class="spinner u-spin" aria-hidden="true"></span>
45
+ <span class="status-text">Waiting for substrate data</span>
46
+ </span>
47
+ <button class="btn btn-primary btn--sm u-press" type="button" id="refresh-button">Refresh</button>
48
+ </div>
49
+ </header>
50
+
51
+ <div class="shell-grid">
52
+ <aside class="filter-panel" aria-label="Global filters">
53
+ <div id="global-filter-container">
54
+ <noscript>Enable JavaScript to use board filters.</noscript>
55
+ </div>
56
+ </aside>
57
+
58
+ <section class="content-wrap" aria-label="Current board view">
59
+ <div class="toolbar-row">
60
+ <span class="crumb">.work / active</span>
61
+ <span class="diagnostics" id="diagnostics-summary">
62
+ <span class="count">0 diagnostics</span>
63
+ </span>
64
+ </div>
65
+ <div id="diagnostics-region" aria-live="polite"></div>
66
+ <main id="view-root" class="u-view-enter" tabindex="-1">
67
+ <div class="view-placeholder empty">
68
+ <span class="glyph" aria-hidden="true">::</span>
69
+ <h1>Board shell ready</h1>
70
+ <p>Kanban, dependency, and table views mount here as their stories land.</p>
71
+ </div>
72
+ </main>
73
+ </section>
74
+ </div>
75
+ </div>
76
+ </body>
77
+ </html>
@@ -0,0 +1,231 @@
1
+ import { deriveFilterOptions } from "/assets/filters.js";
2
+
3
+ const NO_PARENT_LANE = "(no parent)";
4
+ const NULL_SENTINEL = "unassigned";
5
+
6
+ let focusedLane = null;
7
+ let pendingFocusLane = undefined;
8
+
9
+ function textElement(tag, className, text) {
10
+ const element = document.createElement(tag);
11
+ if (className) {
12
+ element.className = className;
13
+ }
14
+ element.textContent = text;
15
+ return element;
16
+ }
17
+
18
+ function stageValue(item) {
19
+ return item?.stage == null || item.stage === "" ? NULL_SENTINEL : String(item.stage);
20
+ }
21
+
22
+ function withNullLast(values) {
23
+ const stages = values.filter((stage) => stage !== NULL_SENTINEL);
24
+ if (values.includes(NULL_SENTINEL)) {
25
+ stages.push(NULL_SENTINEL);
26
+ }
27
+ return stages;
28
+ }
29
+
30
+ export function stageOrder(snapshot, items) {
31
+ const derived = deriveFilterOptions(snapshot).stages;
32
+ const known = new Set(derived);
33
+ const extras = [];
34
+ for (const item of items) {
35
+ const stage = stageValue(item);
36
+ if (!known.has(stage)) {
37
+ known.add(stage);
38
+ extras.push(stage);
39
+ }
40
+ }
41
+ return withNullLast(derived.concat(extras.sort((a, b) => a.localeCompare(b))));
42
+ }
43
+
44
+ export function groupItemsByStage(items, stages) {
45
+ const groups = new Map(stages.map((stage) => [stage, []]));
46
+ for (const item of items) {
47
+ const stage = stageValue(item);
48
+ if (!groups.has(stage)) {
49
+ groups.set(stage, []);
50
+ }
51
+ groups.get(stage).push(item);
52
+ }
53
+ return groups;
54
+ }
55
+
56
+ export function laneIdForItem(item) {
57
+ if (item?.parent) {
58
+ return String(item.parent);
59
+ }
60
+ if (item?.kind === "epic" && item.id) {
61
+ return String(item.id);
62
+ }
63
+ return NO_PARENT_LANE;
64
+ }
65
+
66
+ export function groupItemsByLane(items) {
67
+ const groups = new Map();
68
+ for (const item of items) {
69
+ const laneId = laneIdForItem(item);
70
+ if (!groups.has(laneId)) {
71
+ groups.set(laneId, []);
72
+ }
73
+ groups.get(laneId).push(item);
74
+ }
75
+ return groups;
76
+ }
77
+
78
+ export function laneProgress(items) {
79
+ const done = items.filter((item) => item?.is_terminal || stageValue(item) === "done").length;
80
+ return { done, total: items.length };
81
+ }
82
+
83
+ function renderColumn(stage, items, ctx, columnId) {
84
+ const column = document.createElement("section");
85
+ column.className = "kanban-column";
86
+ column.dataset.stage = stage;
87
+ column.setAttribute("aria-labelledby", columnId);
88
+
89
+ const heading = document.createElement("header");
90
+ heading.className = "kanban-column__header";
91
+ const title = textElement("h2", "", stage);
92
+ title.id = column.getAttribute("aria-labelledby");
93
+ heading.append(title, textElement("span", "kanban-count", String(items.length)));
94
+
95
+ const stack = document.createElement("div");
96
+ stack.className = "kanban-column__cards";
97
+ if (items.length === 0) {
98
+ stack.append(textElement("p", "kanban-empty", "empty"));
99
+ } else {
100
+ for (const item of items) {
101
+ stack.append(ctx.renderCard(item, { compact: true, context: ctx }));
102
+ }
103
+ }
104
+
105
+ column.append(heading, stack);
106
+ return column;
107
+ }
108
+
109
+ function renderFocusStrip(laneIds, activeLane, root, ctx) {
110
+ const strip = document.createElement("nav");
111
+ strip.className = "kanban-lane-strip";
112
+ strip.setAttribute("aria-label", "Kanban lane focus");
113
+
114
+ const allButton = document.createElement("button");
115
+ allButton.className = "filter-chip kanban-lane-chip";
116
+ allButton.type = "button";
117
+ allButton.textContent = "All lanes";
118
+ allButton.dataset.laneAll = "true";
119
+ allButton.setAttribute("aria-pressed", String(activeLane == null));
120
+ allButton.addEventListener("click", () => {
121
+ focusedLane = null;
122
+ pendingFocusLane = null;
123
+ kanbanView.mount(root, ctx);
124
+ });
125
+ strip.append(allButton);
126
+
127
+ for (const laneId of laneIds) {
128
+ const button = document.createElement("button");
129
+ button.className = "filter-chip kanban-lane-chip";
130
+ button.type = "button";
131
+ button.textContent = laneId;
132
+ button.dataset.lane = laneId;
133
+ button.setAttribute("aria-pressed", String(activeLane === laneId));
134
+ button.addEventListener("click", () => {
135
+ focusedLane = laneId;
136
+ pendingFocusLane = laneId;
137
+ kanbanView.mount(root, ctx);
138
+ });
139
+ strip.append(button);
140
+ }
141
+
142
+ return strip;
143
+ }
144
+
145
+ function renderLane(laneId, items, stages, ctx, laneIndex) {
146
+ const lane = document.createElement("section");
147
+ lane.className = "kanban-lane";
148
+ lane.dataset.lane = laneId;
149
+ lane.setAttribute("aria-labelledby", `kanban-lane-${laneIndex}`);
150
+
151
+ const progress = laneProgress(items);
152
+ const header = document.createElement("header");
153
+ header.className = "kanban-lane__header";
154
+ const title = textElement("h2", "", laneId);
155
+ title.id = lane.getAttribute("aria-labelledby");
156
+ const summary = textElement("span", "kanban-progress", `${progress.done}/${progress.total} done`);
157
+ const meter = document.createElement("span");
158
+ meter.className = "kanban-progressbar";
159
+ meter.setAttribute("role", "progressbar");
160
+ meter.setAttribute("aria-valuemin", "0");
161
+ meter.setAttribute("aria-valuemax", String(progress.total));
162
+ meter.setAttribute("aria-valuenow", String(progress.done));
163
+ meter.style.setProperty(
164
+ "--lane-progress",
165
+ progress.total === 0 ? "0%" : `${(progress.done / progress.total) * 100}%`,
166
+ );
167
+ header.append(title, summary, meter);
168
+
169
+ const board = document.createElement("div");
170
+ board.className = "kanban-board";
171
+ board.setAttribute("aria-label", `${laneId} items grouped by stage`);
172
+ const groups = groupItemsByStage(items, stages);
173
+ let columnIndex = 0;
174
+ for (const [stage, stageItems] of groups) {
175
+ board.append(renderColumn(stage, stageItems, ctx, `kanban-stage-${laneIndex}-${columnIndex}`));
176
+ columnIndex += 1;
177
+ }
178
+
179
+ lane.append(header, board);
180
+ return lane;
181
+ }
182
+
183
+ function restoreLaneFocus(root) {
184
+ if (pendingFocusLane === undefined) {
185
+ return;
186
+ }
187
+ const focusLane = pendingFocusLane;
188
+ pendingFocusLane = undefined;
189
+ for (const button of root.querySelectorAll(".kanban-lane-chip")) {
190
+ if ((focusLane == null && button.dataset.laneAll === "true") || button.dataset.lane === focusLane) {
191
+ button.focus({ preventScroll: true });
192
+ return;
193
+ }
194
+ }
195
+ }
196
+
197
+ export const kanbanView = {
198
+ id: "kanban",
199
+ label: "Kanban",
200
+ mount(root, ctx) {
201
+ const items = ctx.visibleItems();
202
+ const snapshot = ctx.getState().snapshot;
203
+ const stages = stageOrder(snapshot, items);
204
+ const lanes = groupItemsByLane(items);
205
+ const laneIds = Array.from(lanes.keys()).sort((a, b) => a.localeCompare(b));
206
+ const activeLane = focusedLane && lanes.has(focusedLane) ? focusedLane : null;
207
+ const visibleLaneIds = activeLane ? [activeLane] : laneIds;
208
+
209
+ const view = document.createElement("div");
210
+ view.className = "kanban-view";
211
+
212
+ const header = document.createElement("header");
213
+ header.className = "kanban-view__header";
214
+ header.append(
215
+ textElement("h1", "", "Kanban"),
216
+ textElement("p", "view-digest", `${items.length} filtered items grouped by lane and workflow stage.`),
217
+ );
218
+
219
+ const lanesRoot = document.createElement("div");
220
+ lanesRoot.className = "kanban-lanes";
221
+ let laneIndex = 0;
222
+ for (const laneId of visibleLaneIds) {
223
+ lanesRoot.append(renderLane(laneId, lanes.get(laneId) || [], stages, ctx, laneIndex));
224
+ laneIndex += 1;
225
+ }
226
+
227
+ view.append(header, renderFocusStrip(laneIds, activeLane, root, ctx), lanesRoot);
228
+ root.replaceChildren(view);
229
+ restoreLaneFocus(root);
230
+ },
231
+ };
@@ -0,0 +1,260 @@
1
+ const SAFE_LINK_PROTOCOLS = new Set(["http:", "https:", "mailto:"]);
2
+
3
+ function el(tag, className) {
4
+ const node = document.createElement(tag);
5
+ if (className) {
6
+ node.className = className;
7
+ }
8
+ return node;
9
+ }
10
+
11
+ function appendText(parent, text) {
12
+ parent.append(document.createTextNode(text));
13
+ }
14
+
15
+ function safeHref(raw) {
16
+ const value = String(raw || "").trim();
17
+ if (!/^(https?:|mailto:)/i.test(value)) {
18
+ return null;
19
+ }
20
+ try {
21
+ const url = new URL(value);
22
+ return SAFE_LINK_PROTOCOLS.has(url.protocol) ? url.href : null;
23
+ } catch (_error) {
24
+ return null;
25
+ }
26
+ }
27
+
28
+ function appendInline(parent, text, options = {}) {
29
+ const allowLinks = options.allowLinks !== false;
30
+ let index = 0;
31
+
32
+ while (index < text.length) {
33
+ if (text.startsWith("**", index)) {
34
+ const end = text.indexOf("**", index + 2);
35
+ if (end !== -1) {
36
+ const strong = document.createElement("strong");
37
+ appendInline(strong, text.slice(index + 2, end), { allowLinks });
38
+ parent.append(strong);
39
+ index = end + 2;
40
+ continue;
41
+ }
42
+ }
43
+
44
+ if (text[index] === "`") {
45
+ const end = text.indexOf("`", index + 1);
46
+ if (end !== -1) {
47
+ const code = document.createElement("code");
48
+ code.textContent = text.slice(index + 1, end);
49
+ parent.append(code);
50
+ index = end + 1;
51
+ continue;
52
+ }
53
+ }
54
+
55
+ if (allowLinks && text[index] === "[") {
56
+ const labelEnd = text.indexOf("]", index + 1);
57
+ const hrefStart = labelEnd === -1 ? -1 : text.indexOf("(", labelEnd);
58
+ const hrefEnd = hrefStart === -1 ? -1 : text.indexOf(")", hrefStart);
59
+ if (labelEnd !== -1 && hrefStart === labelEnd + 1 && hrefEnd !== -1) {
60
+ const label = text.slice(index + 1, labelEnd);
61
+ const rawHref = text.slice(hrefStart + 1, hrefEnd);
62
+ const href = safeHref(rawHref);
63
+ if (href) {
64
+ const anchor = document.createElement("a");
65
+ anchor.setAttribute("href", href);
66
+ anchor.setAttribute("rel", "noreferrer");
67
+ appendInline(anchor, label, { allowLinks: false });
68
+ parent.append(anchor);
69
+ } else {
70
+ appendText(parent, text.slice(index, hrefEnd + 1));
71
+ }
72
+ index = hrefEnd + 1;
73
+ continue;
74
+ }
75
+ }
76
+
77
+ const nextSpecial = ["**", "`", "["]
78
+ .map((token) => text.indexOf(token, index + 1))
79
+ .filter((position) => position !== -1)
80
+ .sort((a, b) => a - b)[0];
81
+ const next = nextSpecial || text.length;
82
+ appendText(parent, text.slice(index, next));
83
+ index = next;
84
+ }
85
+ }
86
+
87
+ function appendParagraph(root, lines) {
88
+ if (lines.length === 0) {
89
+ return;
90
+ }
91
+ const paragraph = document.createElement("p");
92
+ appendInline(paragraph, lines.join(" ").trim());
93
+ root.append(paragraph);
94
+ }
95
+
96
+ function appendList(root, items) {
97
+ if (items.length === 0) {
98
+ return;
99
+ }
100
+ const list = document.createElement("ul");
101
+ for (const item of items) {
102
+ const listItem = document.createElement("li");
103
+ appendInline(listItem, item);
104
+ list.append(listItem);
105
+ }
106
+ root.append(list);
107
+ }
108
+
109
+ function appendQuote(root, lines) {
110
+ if (lines.length === 0) {
111
+ return;
112
+ }
113
+ const quote = document.createElement("blockquote");
114
+ appendParagraph(quote, lines);
115
+ root.append(quote);
116
+ }
117
+
118
+ function flush(root, state) {
119
+ appendParagraph(root, state.paragraph);
120
+ appendList(root, state.list);
121
+ appendQuote(root, state.quote);
122
+ state.paragraph = [];
123
+ state.list = [];
124
+ state.quote = [];
125
+ }
126
+
127
+ function normalizeBody(markdown) {
128
+ return String(markdown || "").replace(/\r\n?/g, "\n");
129
+ }
130
+
131
+ export function renderInlineMarkdown(markdown, options = {}) {
132
+ const fragment = document.createDocumentFragment();
133
+ appendInline(fragment, String(markdown || ""), options);
134
+ return fragment;
135
+ }
136
+
137
+ export function renderMarkdown(markdown, options = { mode: "detail" }) {
138
+ const root = el("div", options.mode === "summary" ? "md md--summary" : "md");
139
+ const state = { paragraph: [], list: [], quote: [] };
140
+ const lines = normalizeBody(markdown).split("\n");
141
+ let inFence = false;
142
+ let fenceLines = [];
143
+
144
+ for (const line of lines) {
145
+ if (line.trim().startsWith("```")) {
146
+ if (inFence) {
147
+ const pre = document.createElement("pre");
148
+ const code = document.createElement("code");
149
+ code.textContent = fenceLines.join("\n");
150
+ pre.append(code);
151
+ root.append(pre);
152
+ fenceLines = [];
153
+ inFence = false;
154
+ } else {
155
+ flush(root, state);
156
+ inFence = true;
157
+ }
158
+ continue;
159
+ }
160
+
161
+ if (inFence) {
162
+ fenceLines.push(line);
163
+ continue;
164
+ }
165
+
166
+ const trimmed = line.trim();
167
+ if (trimmed === "") {
168
+ flush(root, state);
169
+ continue;
170
+ }
171
+
172
+ const heading = /^(#{1,3})\s+(.+)$/.exec(trimmed);
173
+ if (heading) {
174
+ flush(root, state);
175
+ const level = String(Math.min(heading[1].length, 3));
176
+ const node = document.createElement(`h${level}`);
177
+ appendInline(node, heading[2]);
178
+ root.append(node);
179
+ continue;
180
+ }
181
+
182
+ const listItem = /^[-*]\s+(.+)$/.exec(trimmed);
183
+ if (listItem) {
184
+ appendParagraph(root, state.paragraph);
185
+ appendQuote(root, state.quote);
186
+ state.paragraph = [];
187
+ state.quote = [];
188
+ state.list.push(listItem[1]);
189
+ continue;
190
+ }
191
+
192
+ const quote = /^>\s?(.*)$/.exec(trimmed);
193
+ if (quote) {
194
+ appendParagraph(root, state.paragraph);
195
+ appendList(root, state.list);
196
+ state.paragraph = [];
197
+ state.list = [];
198
+ state.quote.push(quote[1]);
199
+ continue;
200
+ }
201
+
202
+ appendList(root, state.list);
203
+ appendQuote(root, state.quote);
204
+ state.list = [];
205
+ state.quote = [];
206
+ state.paragraph.push(trimmed);
207
+ }
208
+
209
+ if (inFence) {
210
+ const pre = document.createElement("pre");
211
+ const code = document.createElement("code");
212
+ code.textContent = fenceLines.join("\n");
213
+ pre.append(code);
214
+ root.append(pre);
215
+ }
216
+ flush(root, state);
217
+
218
+ if (!root.hasChildNodes()) {
219
+ root.append(el("p", ""));
220
+ }
221
+ return root;
222
+ }
223
+
224
+ export function markdownSummary(markdown, maxChars = 220) {
225
+ const lines = normalizeBody(markdown).split("\n");
226
+ const paragraph = [];
227
+ let inFence = false;
228
+
229
+ for (const line of lines) {
230
+ const trimmed = line.trim();
231
+ if (trimmed.startsWith("```")) {
232
+ inFence = !inFence;
233
+ continue;
234
+ }
235
+ if (inFence || /^#{1,3}\s+/.test(trimmed)) {
236
+ continue;
237
+ }
238
+ if (trimmed === "") {
239
+ if (paragraph.length > 0) {
240
+ break;
241
+ }
242
+ continue;
243
+ }
244
+ if (/^[-*]\s+/.test(trimmed)) {
245
+ paragraph.push(trimmed.replace(/^[-*]\s+/, ""));
246
+ break;
247
+ }
248
+ if (/^>\s?/.test(trimmed)) {
249
+ paragraph.push(trimmed.replace(/^>\s?/, ""));
250
+ break;
251
+ }
252
+ paragraph.push(trimmed);
253
+ }
254
+
255
+ const summary = paragraph.join(" ").trim();
256
+ if (summary.length <= maxChars) {
257
+ return summary;
258
+ }
259
+ return `${summary.slice(0, Math.max(0, maxChars - 1)).trimEnd()}...`;
260
+ }
@@ -0,0 +1,131 @@
1
+ /* ============================================================
2
+ * motion.css — Substrate Board (epic-substrate-board) motion system
3
+ *
4
+ * Generated: 2026-05-31 by /ux-ui-design:motion
5
+ * Depends on: tokens.css (link first), composed against components.css
6
+ *
7
+ * Attitude (locked):
8
+ * - Primary: restrained (motion appears only when it aids clarity)
9
+ * - Secondary: productive (efficient, decelerate-fast, stays out of the way)
10
+ * The board is the human twin of a terse CLI — kinetics reinforce speed and
11
+ * clarity, never spectacle.
12
+ *
13
+ * Doherty coupling (locked):
14
+ * --dur-instant 100ms / --dur-quick 180ms / --dur-theme 140ms gate input.
15
+ * --dur-ambient 600ms is BACKGROUND ONLY (spinner). Nothing that gates
16
+ * input exceeds 180ms here — well under the 400ms Doherty threshold.
17
+ *
18
+ * Easing curves (locked):
19
+ * --motion-standard, --motion-productive, --motion-emphasized, --motion-linear
20
+ * (deliberately omitted: --motion-expressive / overshoot — banned for a
21
+ * restrained data tool.)
22
+ *
23
+ * Disney principles: ALL omitted (squash, anticipation, follow-through,
24
+ * secondary). A dense read-only board doesn't bounce. Slow-in/slow-out is
25
+ * carried implicitly by --motion-standard.
26
+ *
27
+ * Optional channels:
28
+ * Springs: omitted (desktop/cursor, read-only — no gestures to honor).
29
+ * Hold beat: --hold-beat 120ms (drawer entry → focus settle).
30
+ * Stepped: omitted (slick digital aesthetic).
31
+ *
32
+ * Reduced-motion: REQUIRED. Every transform/translation is cut under
33
+ * prefers-reduced-motion; opacity + color transitions are kept.
34
+ *
35
+ * Edit by re-running /ux-ui-design:motion (it preserves this header).
36
+ * ============================================================ */
37
+
38
+ :root {
39
+ /* ---- named easing curves (attitude-carrying) ---- */
40
+ --motion-standard: cubic-bezier(0.4, 0, 0.2, 1); /* workhorse: accel from rest, decel to rest */
41
+ --motion-productive: cubic-bezier(0, 0, 0.2, 1); /* decel-only snap-to-final; state changes */
42
+ --motion-emphasized: cubic-bezier(0.2, 0, 0, 1); /* strong decelerate; drawer / overlay entry */
43
+ --motion-linear: linear; /* spinners, physics-defying loops */
44
+
45
+ /* ---- Doherty-coupled durations ---- */
46
+ --dur-instant: 100ms; /* hover / pressed / selected state changes */
47
+ --dur-theme: 140ms; /* accent + light/dark color crossfade (color only, no layout) */
48
+ --dur-quick: 180ms; /* default for input-gating transitions; 300ms ceiling, we sit well under */
49
+ --dur-ambient: 600ms; /* BACKGROUND ONLY — spinner cycle, never gates input */
50
+
51
+ /* ---- designed pause ---- */
52
+ --hold-beat: 120ms; /* stillness between drawer arrival and focus-ring appearing */
53
+
54
+ /* ---- minimal stagger (filter reflow) ---- */
55
+ --stagger-step: 22ms; /* per-item enter delay; cap application at ~8 items (see .u-stagger) */
56
+
57
+ /* ---- press affordance (restrained — a nudge, not a squash) ---- */
58
+ --press-nudge: translateY(0.5px);
59
+ }
60
+
61
+ /* ============================================================ KEYFRAMES
62
+ * Compositor-cheap only: transform + opacity. No layout properties. */
63
+ @keyframes mo-fade-in { from { opacity: 0; } to { opacity: 1; } }
64
+ @keyframes mo-fade-out { from { opacity: 1; } to { opacity: 0; } }
65
+ @keyframes mo-card-enter{ from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }
66
+ @keyframes mo-card-exit { from { opacity: 1; transform: none; } to { opacity: 0; transform: translateY(2px); } }
67
+ @keyframes mo-view-enter{ from { opacity: 0; transform: translateY(3px); } to { opacity: 1; transform: none; } }
68
+ @keyframes mo-drawer-in { from { transform: translateX(100%); } to { transform: translateX(0); } }
69
+ @keyframes mo-drawer-out{ from { transform: translateX(0); } to { transform: translateX(100%); } }
70
+ @keyframes mo-spin { to { transform: rotate(360deg); } }
71
+
72
+ /* ============================================================ UTILITY CLASSES
73
+ * Screens/flows apply these; they reference the tokens above so the kinetic
74
+ * voice stays consistent. */
75
+
76
+ /* theme swap — color/border/shadow only, no layout shift. Put on themed surfaces. */
77
+ .u-theme-anim {
78
+ transition:
79
+ background-color var(--dur-theme) var(--motion-standard),
80
+ border-color var(--dur-theme) var(--motion-standard),
81
+ color var(--dur-theme) var(--motion-standard),
82
+ box-shadow var(--dur-theme) var(--motion-standard),
83
+ fill var(--dur-theme) var(--motion-standard);
84
+ }
85
+
86
+ /* item-card hover / selected (state change — instant class) */
87
+ .u-state { transition: border-color var(--dur-instant) var(--motion-standard),
88
+ box-shadow var(--dur-instant) var(--motion-standard),
89
+ opacity var(--dur-instant) var(--motion-standard); }
90
+
91
+ /* filter reflow — cards entering the visible set */
92
+ .u-card-enter { animation: mo-card-enter var(--dur-quick) var(--motion-productive) both; }
93
+ .u-card-exit { animation: mo-card-exit var(--dur-instant) var(--motion-standard) both; }
94
+ /* minimal stagger: set style="--i:N" per card; cap N at 8 so long lists don't lag */
95
+ .u-stagger > * { animation-delay: calc(min(var(--i, 0), 8) * var(--stagger-step)); }
96
+
97
+ /* view-switch crossfade (Kanban / Dependency / Table) */
98
+ .u-view-enter { animation: mo-view-enter var(--dur-quick) var(--motion-standard) both; }
99
+
100
+ /* read-only detail-drawer slide from the right */
101
+ .u-drawer-in { animation: mo-drawer-in var(--dur-quick) var(--motion-emphasized) both; }
102
+ .u-drawer-out { animation: mo-drawer-out var(--dur-quick) var(--motion-emphasized) both; }
103
+
104
+ /* press nudge (restrained) */
105
+ .u-press:active { transform: var(--press-nudge); }
106
+
107
+ /* spinner timing (indeterminate feed load — allowed infinite loop) */
108
+ .u-spin { animation: mo-spin var(--dur-ambient) var(--motion-linear) infinite; }
109
+
110
+ /* ============================================================ REDUCED MOTION
111
+ * Cut every transform/translation; keep opacity + color. */
112
+ @media (prefers-reduced-motion: reduce) {
113
+ .u-card-enter,
114
+ .u-view-enter,
115
+ .u-drawer-in { animation: mo-fade-in var(--dur-instant) var(--motion-linear) both; }
116
+ .u-card-exit,
117
+ .u-drawer-out { animation: mo-fade-out var(--dur-instant) var(--motion-linear) both; }
118
+ .u-stagger > * { animation-delay: 0ms; }
119
+ .u-press:active { transform: none; }
120
+ .u-spin { animation-duration: 1.8s; } /* slow, low-vestibular; or swap to determinate */
121
+ }
122
+
123
+ /* Showcase-only simulation hook (mirrors the media query for the preview toggle). */
124
+ [data-simulate-rm="on"] .u-card-enter,
125
+ [data-simulate-rm="on"] .u-view-enter,
126
+ [data-simulate-rm="on"] .u-drawer-in { animation: mo-fade-in var(--dur-instant) var(--motion-linear) both; }
127
+ [data-simulate-rm="on"] .u-card-exit,
128
+ [data-simulate-rm="on"] .u-drawer-out { animation: mo-fade-out var(--dur-instant) var(--motion-linear) both; }
129
+ [data-simulate-rm="on"] .u-stagger > * { animation-delay: 0ms; }
130
+ [data-simulate-rm="on"] .u-press:active { transform: none; }
131
+ [data-simulate-rm="on"] .u-spin { animation-duration: 1.8s; }