@wanbicoi/next-steps 0.0.0-stage → 0.1.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.
package/package.json CHANGED
@@ -1,6 +1,61 @@
1
1
  {
2
2
  "name": "@wanbicoi/next-steps",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.1",
4
+ "description": "Agent-facing workflow walker: log a step, list the next ones.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "bin": {
8
+ "next-steps": "./dist/cli.js"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "viz/template.html",
13
+ "README.md",
14
+ "LICENSE"
15
+ ],
16
+ "engines": {
17
+ "node": ">=22.13.0"
18
+ },
19
+ "keywords": [
20
+ "workflow",
21
+ "agent",
22
+ "cli",
23
+ "state-machine",
24
+ "next-steps"
25
+ ],
26
+ "publishConfig": {
27
+ "access": "public"
28
+ },
29
+ "scripts": {
30
+ "next-steps": "bun src/cli.ts",
31
+ "dev": "bun --watch src/cli.ts",
32
+ "build": "bun scripts/build.ts",
33
+ "prepack": "bun run build",
34
+ "prepublishOnly": "bun run lint && bun run typecheck && bun test && bun run knip",
35
+ "test": "bun test",
36
+ "typecheck": "bunx tsc --noEmit",
37
+ "lint": "biome check .",
38
+ "lint:fix": "biome check --write .",
39
+ "format": "biome format --write .",
40
+ "knip": "knip",
41
+ "prepare": "husky"
42
+ },
43
+ "lint-staged": {
44
+ "*.{ts,js,mjs,json,md}": "biome check --write --no-errors-on-unmatched --files-ignore-unknown=true"
45
+ },
46
+ "dependencies": {
47
+ "cytoscape": "^3.34.4",
48
+ "cytoscape-dagre": "^4.0.1",
49
+ "zod": "^4.6.7"
50
+ },
51
+ "devDependencies": {
52
+ "@biomejs/biome": "^2.6.4",
53
+ "@commitlint/cli": "^21.2.4",
54
+ "@commitlint/config-conventional": "^21.2.4",
55
+ "@types/bun": "^1.4.4",
56
+ "husky": "^9.1.7",
57
+ "knip": "^6.44.1",
58
+ "lint-staged": "^17.6.0",
59
+ "typescript": "^7.0.2"
60
+ }
61
+ }
@@ -0,0 +1,648 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>next-steps workflow viewer</title>
7
+ <style>
8
+ :root {
9
+ --bg: #f8fafc;
10
+ --panel: #ffffff;
11
+ --border: #e2e8f0;
12
+ --text: #0f172a;
13
+ --muted: #64748b;
14
+ --accent: #2563eb;
15
+ --accent-soft: #eff6ff;
16
+ --code: #f1f5f9;
17
+ --font-ui: system-ui, -apple-system, "Segoe UI", Roboto, "Noto Sans", "DejaVu Sans",
18
+ sans-serif;
19
+ --font-mono: ui-monospace, "SF Mono", "Cascadia Code", Menlo, Consolas, "DejaVu Sans Mono",
20
+ monospace;
21
+ }
22
+ * {
23
+ box-sizing: border-box;
24
+ }
25
+ html,
26
+ body {
27
+ height: 100%;
28
+ }
29
+ body {
30
+ margin: 0;
31
+ font: 13px/1.5 var(--font-ui);
32
+ color: var(--text);
33
+ background: var(--bg);
34
+ }
35
+ #app {
36
+ display: grid;
37
+ grid-template-columns: 250px minmax(0, 1fr) 300px;
38
+ height: 100vh;
39
+ overflow: hidden;
40
+ }
41
+ aside,
42
+ main {
43
+ min-width: 0;
44
+ min-height: 0;
45
+ }
46
+ #sidebar {
47
+ display: flex;
48
+ flex-direction: column;
49
+ border-right: 1px solid var(--border);
50
+ background: var(--panel);
51
+ overflow: hidden;
52
+ }
53
+ #sidebar header,
54
+ #details {
55
+ padding: 16px;
56
+ }
57
+ #sidebar header {
58
+ border-bottom: 1px solid var(--border);
59
+ }
60
+ h1 {
61
+ margin: 0;
62
+ font-size: 15px;
63
+ letter-spacing: -0.01em;
64
+ }
65
+ h1 span {
66
+ color: var(--muted);
67
+ font-weight: 500;
68
+ }
69
+ #count {
70
+ margin: 4px 0 12px;
71
+ color: var(--muted);
72
+ font-size: 12px;
73
+ }
74
+ #legend {
75
+ border-top: 1px solid var(--border);
76
+ padding: 12px 16px 16px;
77
+ font-size: 12px;
78
+ color: var(--muted);
79
+ }
80
+ #legend div {
81
+ display: flex;
82
+ align-items: center;
83
+ gap: 8px;
84
+ margin-top: 6px;
85
+ }
86
+ .swatch {
87
+ width: 14px;
88
+ height: 14px;
89
+ border-radius: 4px;
90
+ border: 2px solid;
91
+ flex: none;
92
+ }
93
+ main {
94
+ display: flex;
95
+ flex-direction: column;
96
+ overflow: hidden;
97
+ }
98
+ #toolbar {
99
+ display: flex;
100
+ align-items: center;
101
+ justify-content: space-between;
102
+ gap: 16px;
103
+ padding: 12px 16px;
104
+ border-bottom: 1px solid var(--border);
105
+ background: var(--panel);
106
+ }
107
+ #toolbar h2 {
108
+ margin: 0;
109
+ font-size: 14px;
110
+ }
111
+ #subtitle {
112
+ color: var(--muted);
113
+ font-size: 12px;
114
+ font-family: var(--font-mono);
115
+ }
116
+ #controls {
117
+ display: flex;
118
+ align-items: center;
119
+ gap: 8px;
120
+ flex-wrap: wrap;
121
+ }
122
+ button,
123
+ label.toggle {
124
+ font: inherit;
125
+ font-size: 12px;
126
+ color: var(--text);
127
+ background: var(--panel);
128
+ border: 1px solid var(--border);
129
+ border-radius: 7px;
130
+ padding: 6px 9px;
131
+ cursor: pointer;
132
+ }
133
+ button:hover,
134
+ label.toggle:hover {
135
+ background: var(--bg);
136
+ }
137
+ button:active {
138
+ transform: translateY(1px);
139
+ }
140
+ button.primary {
141
+ color: #fff;
142
+ background: var(--accent);
143
+ border-color: var(--accent);
144
+ }
145
+ #cy {
146
+ flex: 1;
147
+ min-height: 0;
148
+ background: radial-gradient(circle at 1px 1px, #e2e8f0 1px, transparent 0) 0 0 / 20px 20px;
149
+ }
150
+ #details {
151
+ border-left: 1px solid var(--border);
152
+ background: var(--panel);
153
+ overflow: auto;
154
+ }
155
+ #details h3 {
156
+ margin: 0 0 4px;
157
+ font-size: 14px;
158
+ }
159
+ #details .muted {
160
+ color: var(--muted);
161
+ }
162
+ #details dl {
163
+ margin: 12px 0 0;
164
+ display: grid;
165
+ grid-template-columns: 72px 1fr;
166
+ gap: 6px 10px;
167
+ }
168
+ #details dt {
169
+ color: var(--muted);
170
+ }
171
+ #details dd {
172
+ margin: 0;
173
+ word-break: break-word;
174
+ }
175
+ #details code {
176
+ background: var(--code);
177
+ padding: 1px 4px;
178
+ border-radius: 4px;
179
+ font-family: var(--font-mono);
180
+ font-size: 11px;
181
+ }
182
+ #details .hint {
183
+ margin-top: 16px;
184
+ padding: 10px;
185
+ border-radius: 7px;
186
+ background: var(--bg);
187
+ color: var(--muted);
188
+ font-size: 12px;
189
+ }
190
+ #details button {
191
+ margin-top: 12px;
192
+ }
193
+ @media (max-width: 1100px) {
194
+ #app {
195
+ grid-template-columns: 220px minmax(0, 1fr);
196
+ }
197
+ #details {
198
+ display: none;
199
+ }
200
+ }
201
+ @media (max-width: 720px) {
202
+ #app {
203
+ grid-template-columns: 1fr;
204
+ }
205
+ #sidebar {
206
+ display: none;
207
+ }
208
+ }
209
+ </style>
210
+ </head>
211
+ <body>
212
+ <div id="app">
213
+ <aside id="sidebar">
214
+ <header>
215
+ <h1>next-steps <span>viewer</span></h1>
216
+ <p id="count"></p>
217
+ </header>
218
+ <section id="legend">
219
+ <strong>Legend</strong>
220
+ <div><span class="swatch" style="background:#fff;border-color:#94a3b8"></span> Node</div>
221
+ <div><span class="swatch" style="background:#f5f3ff;border-color:#7c3aed"></span> Group</div>
222
+ <div><span class="swatch" style="background:#f0fdf4;border-color:#16a34a"></span> Start</div>
223
+ <div><span class="swatch" style="background:#fff;border-color:#f59e0b"></span> Needs review</div>
224
+ <div><span class="swatch" style="background:#0f172a;border-color:#0f172a;border-radius:50%"></span> END</div>
225
+ <div>
226
+ <span class="swatch" style="background:#f59e0b;border-color:#f59e0b;height:3px;border-radius:2px"></span>
227
+ Conditional move
228
+ </div>
229
+ </section>
230
+ </aside>
231
+ <main>
232
+ <header id="toolbar">
233
+ <div>
234
+ <h2 id="title">Loading…</h2>
235
+ <div id="subtitle"></div>
236
+ </div>
237
+ <div id="controls">
238
+ <label class="toggle">
239
+ <input id="show-edge-labels" type="checkbox" checked />
240
+ Edge labels
241
+ </label>
242
+ <label class="toggle">
243
+ <input id="show-conditions" type="checkbox" checked />
244
+ Conditions
245
+ </label>
246
+ <button id="fit" type="button">Fit</button>
247
+ <button id="relayout" type="button">Relayout</button>
248
+ <button id="png" type="button">PNG</button>
249
+ </div>
250
+ </header>
251
+ <div id="cy"></div>
252
+ </main>
253
+ <aside id="details">
254
+ <h3>Workflow viewer</h3>
255
+ <p class="muted">Click a node or an edge. Each box is a group; nodes marked
256
+ <strong>[review]</strong> need an explicit yes before the work starts.</p>
257
+ <div class="hint">
258
+ The data is generated from <code>.next-steps/workflow.json</code>. Run
259
+ <code>bun run viz</code> after editing the graph.
260
+ </div>
261
+ </aside>
262
+ </div>
263
+
264
+ <script src="/vendor/cytoscape.min.js"></script>
265
+ <script src="/vendor/cytoscape-dagre.js"></script>
266
+ <script>
267
+ const DATA = __WORKFLOW_DATA__;
268
+
269
+ if (typeof cytoscape === "undefined") {
270
+ document.getElementById("details").innerHTML =
271
+ '<h3>Could not load Cytoscape</h3><p class="muted">Check the jsDelivr CDN connection.</p>';
272
+ throw new Error("cytoscape is not available");
273
+ }
274
+
275
+ if (typeof cytoscapeDagre !== "undefined") {
276
+ cytoscape.use(cytoscapeDagre);
277
+ }
278
+
279
+ const $ = (selector) => document.querySelector(selector);
280
+
281
+ // Cytoscape draws labels on canvas, so it cannot inherit the page font.
282
+ // Read the same token the CSS uses to keep one source of truth.
283
+ const FONT_UI = getComputedStyle(document.documentElement)
284
+ .getPropertyValue("--font-ui")
285
+ .replace(/\s+/g, " ")
286
+ .trim();
287
+
288
+ const COLORS = {
289
+ nodeBg: "#ffffff",
290
+ nodeBorder: "#94a3b8",
291
+ nodeText: "#0f172a",
292
+ groupBg: "#f5f3ff",
293
+ groupBorder: "#7c3aed",
294
+ startBg: "#f0fdf4",
295
+ startBorder: "#16a34a",
296
+ reviewBorder: "#f59e0b",
297
+ endBg: "#0f172a",
298
+ endText: "#ffffff",
299
+ edge: "#94a3b8",
300
+ conditional: "#f59e0b",
301
+ accent: "#2563eb",
302
+ };
303
+
304
+ // Customise the graph here. Cytoscape selectors support classes,
305
+ // data attributes, and states (`:selected`).
306
+ const STYLE = [
307
+ {
308
+ selector: "node",
309
+ style: {
310
+ "background-color": COLORS.nodeBg,
311
+ "border-color": COLORS.nodeBorder,
312
+ "border-width": 2,
313
+ shape: "round-rectangle",
314
+ width: "label",
315
+ height: "label",
316
+ padding: 10,
317
+ label: "data(label)",
318
+ "font-family": FONT_UI,
319
+ color: COLORS.nodeText,
320
+ "font-size": 12,
321
+ "text-wrap": "wrap",
322
+ "text-max-width": "150px",
323
+ "text-valign": "center",
324
+ "text-halign": "center",
325
+ },
326
+ },
327
+ {
328
+ selector: "node.group",
329
+ style: {
330
+ "background-color": COLORS.groupBg,
331
+ "background-opacity": 0.7,
332
+ "border-color": COLORS.groupBorder,
333
+ "border-width": 1,
334
+ "border-style": "dashed",
335
+ shape: "round-rectangle",
336
+ label: "data(label)",
337
+ "font-family": FONT_UI,
338
+ "font-size": 13,
339
+ "font-weight": "bold",
340
+ color: COLORS.groupBorder,
341
+ "text-valign": "top",
342
+ "text-halign": "center",
343
+ padding: 26,
344
+ },
345
+ },
346
+ {
347
+ selector: "node.start",
348
+ style: {
349
+ "background-color": COLORS.startBg,
350
+ "border-color": COLORS.startBorder,
351
+ "border-width": 3,
352
+ },
353
+ },
354
+ {
355
+ selector: "node.review",
356
+ style: {
357
+ "border-color": COLORS.reviewBorder,
358
+ "border-width": 3,
359
+ },
360
+ },
361
+ {
362
+ selector: "node.end",
363
+ style: {
364
+ "background-color": COLORS.endBg,
365
+ "border-color": COLORS.endBg,
366
+ color: COLORS.endText,
367
+ shape: "ellipse",
368
+ width: 52,
369
+ height: 52,
370
+ "font-family": FONT_UI,
371
+ "font-size": 12,
372
+ "font-weight": "bold",
373
+ },
374
+ },
375
+ {
376
+ selector: "node:selected",
377
+ style: {
378
+ "border-color": COLORS.accent,
379
+ "border-width": 4,
380
+ },
381
+ },
382
+ {
383
+ selector: "edge",
384
+ style: {
385
+ width: 1.5,
386
+ "line-color": COLORS.edge,
387
+ "target-arrow-color": COLORS.edge,
388
+ "target-arrow-shape": "triangle",
389
+ "curve-style": "bezier",
390
+ label: "data(label)",
391
+ "font-family": FONT_UI,
392
+ "font-size": 10,
393
+ color: "#475569",
394
+ "text-wrap": "wrap",
395
+ "text-max-width": "140px",
396
+ "text-rotation": "autorotate",
397
+ "text-background-color": "#ffffff",
398
+ "text-background-opacity": 0.9,
399
+ "text-background-padding": 2,
400
+ },
401
+ },
402
+ {
403
+ selector: "edge.conditional",
404
+ style: {
405
+ "line-style": "dashed",
406
+ "line-color": COLORS.conditional,
407
+ "target-arrow-color": COLORS.conditional,
408
+ },
409
+ },
410
+ {
411
+ selector: "edge:selected",
412
+ style: {
413
+ width: 3,
414
+ "line-color": COLORS.accent,
415
+ "target-arrow-color": COLORS.accent,
416
+ },
417
+ },
418
+ ];
419
+ const state = {
420
+ labels: true,
421
+ conditions: true,
422
+ };
423
+
424
+ let cy = null;
425
+
426
+ function esc(value) {
427
+ return String(value ?? "").replace(
428
+ /[&<>"']/g,
429
+ (ch) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[ch],
430
+ );
431
+ }
432
+
433
+ function edgeLabel(move) {
434
+ if (!state.labels) return "";
435
+ const parts = [move.description || ""];
436
+ if (state.conditions && move.condition) parts.push(`when: ${move.condition}`);
437
+ return parts.filter(Boolean).join("\n");
438
+ }
439
+
440
+ function render() {
441
+ const workflow = DATA.workflow;
442
+ $("#title").textContent = workflow.title;
443
+ $("#subtitle").textContent = `${workflow.id} · start: ${workflow.start}`;
444
+ $("#count").textContent = `${workflow.nodes.length} nodes`;
445
+ document.title = `${workflow.title} · next-steps`;
446
+ renderGraph(workflow);
447
+ }
448
+
449
+ function renderGraph(workflow) {
450
+ const elements = [];
451
+ const groups = new Map();
452
+ for (const node of workflow.nodes) {
453
+ if (!node.group || groups.has(node.group)) continue;
454
+ groups.set(node.group, `group-${groups.size}`);
455
+ }
456
+
457
+ for (const [label, id] of groups) {
458
+ elements.push({ data: { id, label }, classes: "group" });
459
+ }
460
+
461
+ for (const node of workflow.nodes) {
462
+ const classes = ["node"];
463
+ if (node.id === workflow.start) classes.push("start");
464
+ if (node.reviewRequired) classes.push("review");
465
+ elements.push({
466
+ data: {
467
+ id: node.id,
468
+ label: node.reviewRequired ? `${node.title} [review]` : node.title,
469
+ title: node.title,
470
+ description: node.description,
471
+ group: node.group || "",
472
+ reviewRequired: Boolean(node.reviewRequired),
473
+ ...(node.group ? { parent: groups.get(node.group) } : {}),
474
+ },
475
+ classes: classes.join(" "),
476
+ });
477
+ }
478
+
479
+ const moves = workflow.nodes.flatMap((node) =>
480
+ node.next.map((move) => ({ from: node.id, ...move })),
481
+ );
482
+
483
+ if (moves.some((move) => move.to === "END")) {
484
+ elements.push({
485
+ data: {
486
+ id: "END",
487
+ label: "END",
488
+ title: "END",
489
+ description: "The run is finished.",
490
+ },
491
+ classes: "end",
492
+ });
493
+ }
494
+
495
+ for (const move of moves) {
496
+ elements.push({
497
+ data: {
498
+ id: `${move.from}->${move.to}`,
499
+ source: move.from,
500
+ target: move.to,
501
+ label: edgeLabel(move),
502
+ from: move.from,
503
+ to: move.to,
504
+ description: move.description,
505
+ condition: move.condition || "",
506
+ },
507
+ classes: move.condition ? "conditional" : "edge",
508
+ });
509
+ }
510
+
511
+ if (cy) {
512
+ cy.destroy();
513
+ cy = null;
514
+ }
515
+
516
+ cy = cytoscape({
517
+ container: $("#cy"),
518
+ elements,
519
+ style: STYLE,
520
+ wheelSensitivity: 1,
521
+ minZoom: 0.15,
522
+ maxZoom: 2.5,
523
+ });
524
+
525
+ cy.on("tap", "node.node", (event) => showNode(event.target));
526
+ cy.on("tap", "node.end", (event) => showNode(event.target));
527
+ cy.on("tap", "edge", (event) => showEdge(event.target));
528
+ cy.on("tap", (event) => {
529
+ if (event.target === cy) showIntro();
530
+ });
531
+
532
+ cy.ready(() => runLayout());
533
+ showIntro();
534
+ }
535
+
536
+ function runLayout() {
537
+ if (!cy) return;
538
+ try {
539
+ cy.layout({
540
+ name: "dagre",
541
+ rankDir: "TB",
542
+ nodeSep: 35,
543
+ rankSep: 80,
544
+ edgeSep: 20,
545
+ padding: 30,
546
+ animate: false,
547
+ fit: true,
548
+ }).run();
549
+ } catch {
550
+ cy.layout({
551
+ name: "breadthfirst",
552
+ directed: true,
553
+ padding: 30,
554
+ animate: false,
555
+ fit: true,
556
+ }).run();
557
+ }
558
+ cy.fit(undefined, 30);
559
+ }
560
+
561
+ function refreshEdgeLabels() {
562
+ if (!cy) return;
563
+ cy.edges().forEach((edge) => {
564
+ const data = edge.data();
565
+ edge.data(
566
+ "label",
567
+ edgeLabel({ description: data.description, condition: data.condition }),
568
+ );
569
+ });
570
+ }
571
+
572
+ function showIntro() {
573
+ $("#details").innerHTML = `
574
+ <h3>Workflow viewer</h3>
575
+ <p class="muted">Click a node or an edge. Each box is a group; nodes marked
576
+ <strong>[review]</strong> need an explicit yes before the work starts.</p>
577
+ <div class="hint">
578
+ The data is generated from <code>.next-steps/workflow.json</code>. Run
579
+ <code>bun run viz</code> after editing the graph.
580
+ </div>
581
+ `;
582
+ }
583
+
584
+ function showNode(node) {
585
+ const data = node.data();
586
+ const group = data.group ? `<dt>Group</dt><dd>${esc(data.group)}</dd>` : "";
587
+ const review = data.reviewRequired
588
+ ? "<dt>Review</dt><dd>Show the plan, wait for an explicit yes, then act.</dd>"
589
+ : "";
590
+
591
+ $("#details").innerHTML = `
592
+ <h3>${esc(data.title)}</h3>
593
+ <div class="muted"><code>${esc(data.id)}</code>${data.id === "END" ? " · END" : ""}</div>
594
+ <dl>
595
+ <dt>Description</dt><dd>${esc(data.description || "—")}</dd>
596
+ ${group}
597
+ ${review}
598
+ </dl>
599
+ `;
600
+
601
+ if (data.id !== "END") {
602
+ const hint = document.createElement("div");
603
+ hint.className = "hint";
604
+ hint.innerHTML = `Log this step with <code>next-steps log --key &lt;key&gt; --node ${esc(
605
+ data.id,
606
+ )}</code>.`;
607
+ $("#details").appendChild(hint);
608
+ }
609
+ }
610
+
611
+ function showEdge(edge) {
612
+ const data = edge.data();
613
+ $("#details").innerHTML = `
614
+ <h3>${esc(data.from)} → ${esc(data.to)}</h3>
615
+ <dl>
616
+ <dt>Description</dt><dd>${esc(data.description || "—")}</dd>
617
+ <dt>Condition</dt><dd>${esc(data.condition || "—")}</dd>
618
+ </dl>
619
+ <div class="hint">
620
+ Conditions are prose. The agent decides; the logged move is the audit trail.
621
+ </div>
622
+ `;
623
+ }
624
+
625
+ function exportPng() {
626
+ if (!cy) return;
627
+ const link = document.createElement("a");
628
+ link.href = cy.png({ full: true, scale: 2, bg: "#ffffff" });
629
+ link.download = `${DATA.workflow.id}.png`;
630
+ link.click();
631
+ }
632
+
633
+ $("#fit").addEventListener("click", () => cy?.fit(undefined, 30));
634
+ $("#relayout").addEventListener("click", runLayout);
635
+ $("#png").addEventListener("click", exportPng);
636
+ $("#show-edge-labels").addEventListener("change", (event) => {
637
+ state.labels = event.target.checked;
638
+ refreshEdgeLabels();
639
+ });
640
+ $("#show-conditions").addEventListener("change", (event) => {
641
+ state.conditions = event.target.checked;
642
+ refreshEdgeLabels();
643
+ });
644
+
645
+ render();
646
+ </script>
647
+ </body>
648
+ </html>