@wanbicoi/next-steps 0.1.0 → 0.1.2
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/README.md +124 -50
- package/dist/cli.js +444 -621
- package/docs/AGENTS.sample.md +120 -0
- package/package.json +2 -1
- package/viz/template.html +106 -165
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
<!-- Template: copy this to AGENTS.md in a repo that uses next-steps on PATH. -->
|
|
2
|
+
|
|
3
|
+
# Work the graph with next-steps
|
|
4
|
+
|
|
5
|
+
This repo defines its work as one graph in `.next-steps/workflow.json`.
|
|
6
|
+
`next-steps` walks that graph one node at a time: it tells you which nodes are
|
|
7
|
+
legal now, and records the path you take.
|
|
8
|
+
|
|
9
|
+
Pass `--json` to every command.
|
|
10
|
+
|
|
11
|
+
## Walk
|
|
12
|
+
|
|
13
|
+
1. **Open a session.**
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
next-steps create --json
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
The response holds `key` (your task id) and the first `next` list.
|
|
20
|
+
|
|
21
|
+
**Done when:** you have `key` in hand and a non-empty `next` list.
|
|
22
|
+
|
|
23
|
+
2. **Choose one next node.**
|
|
24
|
+
|
|
25
|
+
Each entry has `node`, `edge`, `title`, `description`, `reviewRequired`, and
|
|
26
|
+
sometimes `condition`. The `condition` is prose telling you when that branch
|
|
27
|
+
applies; you judge it against the task, nothing checks it for you. `edge` is
|
|
28
|
+
derived from `from->to` and is what the log will record.
|
|
29
|
+
|
|
30
|
+
```json
|
|
31
|
+
{
|
|
32
|
+
"current": { "node": "verify", "title": "Verify" },
|
|
33
|
+
"next": [
|
|
34
|
+
{ "edge": "verify->implement", "node": "implement", "title": "Implement",
|
|
35
|
+
"description": "Loop back and fix the failure.",
|
|
36
|
+
"condition": "the build or the tests failed",
|
|
37
|
+
"reviewRequired": true },
|
|
38
|
+
{ "edge": "verify->review", "node": "review", "title": "Peer review",
|
|
39
|
+
"description": "Hand the change to review.",
|
|
40
|
+
"condition": "the build and the tests pass",
|
|
41
|
+
"reviewRequired": false }
|
|
42
|
+
],
|
|
43
|
+
"done": false
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**Done when:** exactly one entry is chosen.
|
|
48
|
+
|
|
49
|
+
3. **If that entry has `reviewRequired: true`, stop and ask.**
|
|
50
|
+
|
|
51
|
+
Prepare everything the node needs — the plan, the diff, the command you are
|
|
52
|
+
about to run — show it to the user, and wait for an explicit yes. Then act.
|
|
53
|
+
|
|
54
|
+
**Done when:** the user said yes. The CLI only reports the flag — nothing
|
|
55
|
+
enforces it. A node without the flag: skip this step.
|
|
56
|
+
|
|
57
|
+
4. **Do that node's work.**
|
|
58
|
+
|
|
59
|
+
**Done when:** the node's `description` is satisfied — not when you started
|
|
60
|
+
it.
|
|
61
|
+
|
|
62
|
+
5. **Log the node.**
|
|
63
|
+
|
|
64
|
+
```sh
|
|
65
|
+
next-steps log --key <key> --node <node> --json
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
One log per visit, after the work. Log is the only call per node; the
|
|
69
|
+
response already carries the new `next` list, so continue straight from it.
|
|
70
|
+
|
|
71
|
+
**Done when:** exit `0` and the response contains the new `next` list.
|
|
72
|
+
|
|
73
|
+
6. **Repeat 2–5 until the response says `done: true`.**
|
|
74
|
+
|
|
75
|
+
**Done when:** `done` is true. Stop there; a written summary is its own node
|
|
76
|
+
if the graph wants one.
|
|
77
|
+
|
|
78
|
+
`next-steps next --key <key> --json` returns the same list on demand. Use it when
|
|
79
|
+
you resume rather than during the walk.
|
|
80
|
+
|
|
81
|
+
## Branches
|
|
82
|
+
|
|
83
|
+
- **`END` is a legal target.** Log it like any node; the session becomes
|
|
84
|
+
`done`. The `next` list also labels it `finish`.
|
|
85
|
+
- **A node whose only move is `END`** finishes the run as soon as you log it.
|
|
86
|
+
The response is already `done: true`; there is no extra step.
|
|
87
|
+
- **Exit `4`** means the move is not legal. The error's `details.legal` is the
|
|
88
|
+
complete list of nodes you can move to; choose from it, or correct the work and
|
|
89
|
+
log the node the graph expects.
|
|
90
|
+
- **Exit `3`** means the key is unknown or `.next-steps/workflow.json` is
|
|
91
|
+
missing — run `next-steps workflow new` if the graph has never been created. A
|
|
92
|
+
key belongs to one task: create a fresh session for a new task.
|
|
93
|
+
|
|
94
|
+
## Resume
|
|
95
|
+
|
|
96
|
+
A new agent, or the same one after a context reset, rebuilds the task from the
|
|
97
|
+
tool:
|
|
98
|
+
|
|
99
|
+
```sh
|
|
100
|
+
next-steps steps --key <key> --json # audit trail: seq, node, from, edge, at
|
|
101
|
+
next-steps next --key <key> --json # legal moves now
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
The `steps` record is the source of truth for what already happened.
|
|
105
|
+
|
|
106
|
+
## Housekeeping
|
|
107
|
+
|
|
108
|
+
```sh
|
|
109
|
+
next-steps keys --json # sessions on this machine
|
|
110
|
+
next-steps reset --key <key> --json # delete a finished or abandoned session
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
`.next-steps/state.db` is local state and is gitignored. The graph,
|
|
114
|
+
`.next-steps/workflow.json`, is source code: commit it with the rest of the repo.
|
|
115
|
+
|
|
116
|
+
## Exit codes
|
|
117
|
+
|
|
118
|
+
`0` ok · `2` usage, duplicate key, or invalid key · `3` not found ·
|
|
119
|
+
`4` invalid move or unknown node · `5` invalid workflow file. Errors go to
|
|
120
|
+
stderr as `{ "error": { "code", "message", "details" } }`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wanbicoi/next-steps",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Agent-facing workflow walker: log a step, list the next ones.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"files": [
|
|
11
11
|
"dist",
|
|
12
12
|
"viz/template.html",
|
|
13
|
+
"docs/AGENTS.sample.md",
|
|
13
14
|
"README.md",
|
|
14
15
|
"LICENSE"
|
|
15
16
|
],
|
package/viz/template.html
CHANGED
|
@@ -14,6 +14,10 @@
|
|
|
14
14
|
--accent: #2563eb;
|
|
15
15
|
--accent-soft: #eff6ff;
|
|
16
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;
|
|
17
21
|
}
|
|
18
22
|
* {
|
|
19
23
|
box-sizing: border-box;
|
|
@@ -24,7 +28,7 @@
|
|
|
24
28
|
}
|
|
25
29
|
body {
|
|
26
30
|
margin: 0;
|
|
27
|
-
font: 13px/1.
|
|
31
|
+
font: 13px/1.5 var(--font-ui);
|
|
28
32
|
color: var(--text);
|
|
29
33
|
background: var(--bg);
|
|
30
34
|
}
|
|
@@ -67,54 +71,6 @@
|
|
|
67
71
|
color: var(--muted);
|
|
68
72
|
font-size: 12px;
|
|
69
73
|
}
|
|
70
|
-
#search {
|
|
71
|
-
width: 100%;
|
|
72
|
-
padding: 7px 9px;
|
|
73
|
-
border: 1px solid var(--border);
|
|
74
|
-
border-radius: 7px;
|
|
75
|
-
font: inherit;
|
|
76
|
-
background: var(--bg);
|
|
77
|
-
color: var(--text);
|
|
78
|
-
outline: none;
|
|
79
|
-
}
|
|
80
|
-
#search:focus {
|
|
81
|
-
border-color: var(--accent);
|
|
82
|
-
box-shadow: 0 0 0 2px var(--accent-soft);
|
|
83
|
-
}
|
|
84
|
-
#workflow-list {
|
|
85
|
-
flex: 1;
|
|
86
|
-
overflow: auto;
|
|
87
|
-
padding: 8px;
|
|
88
|
-
}
|
|
89
|
-
.wf {
|
|
90
|
-
display: block;
|
|
91
|
-
width: 100%;
|
|
92
|
-
padding: 8px 10px;
|
|
93
|
-
margin: 2px 0;
|
|
94
|
-
border: 1px solid transparent;
|
|
95
|
-
border-radius: 7px;
|
|
96
|
-
background: transparent;
|
|
97
|
-
text-align: left;
|
|
98
|
-
font: inherit;
|
|
99
|
-
color: var(--text);
|
|
100
|
-
cursor: pointer;
|
|
101
|
-
}
|
|
102
|
-
.wf:hover {
|
|
103
|
-
background: var(--bg);
|
|
104
|
-
}
|
|
105
|
-
.wf.active {
|
|
106
|
-
background: var(--accent-soft);
|
|
107
|
-
border-color: #bfdbfe;
|
|
108
|
-
}
|
|
109
|
-
.wf strong {
|
|
110
|
-
display: block;
|
|
111
|
-
font-weight: 600;
|
|
112
|
-
}
|
|
113
|
-
.wf small {
|
|
114
|
-
color: var(--muted);
|
|
115
|
-
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
116
|
-
font-size: 11px;
|
|
117
|
-
}
|
|
118
74
|
#legend {
|
|
119
75
|
border-top: 1px solid var(--border);
|
|
120
76
|
padding: 12px 16px 16px;
|
|
@@ -155,7 +111,7 @@
|
|
|
155
111
|
#subtitle {
|
|
156
112
|
color: var(--muted);
|
|
157
113
|
font-size: 12px;
|
|
158
|
-
font-family:
|
|
114
|
+
font-family: var(--font-mono);
|
|
159
115
|
}
|
|
160
116
|
#controls {
|
|
161
117
|
display: flex;
|
|
@@ -186,12 +142,6 @@
|
|
|
186
142
|
background: var(--accent);
|
|
187
143
|
border-color: var(--accent);
|
|
188
144
|
}
|
|
189
|
-
#back {
|
|
190
|
-
display: none;
|
|
191
|
-
}
|
|
192
|
-
#back.visible {
|
|
193
|
-
display: inline-block;
|
|
194
|
-
}
|
|
195
145
|
#cy {
|
|
196
146
|
flex: 1;
|
|
197
147
|
min-height: 0;
|
|
@@ -226,6 +176,7 @@
|
|
|
226
176
|
background: var(--code);
|
|
227
177
|
padding: 1px 4px;
|
|
228
178
|
border-radius: 4px;
|
|
179
|
+
font-family: var(--font-mono);
|
|
229
180
|
font-size: 11px;
|
|
230
181
|
}
|
|
231
182
|
#details .hint {
|
|
@@ -263,25 +214,23 @@
|
|
|
263
214
|
<header>
|
|
264
215
|
<h1>next-steps <span>viewer</span></h1>
|
|
265
216
|
<p id="count"></p>
|
|
266
|
-
<input id="search" type="search" placeholder="Filter workflows" autocomplete="off" />
|
|
267
217
|
</header>
|
|
268
|
-
<nav id="workflow-list"></nav>
|
|
269
218
|
<section id="legend">
|
|
270
219
|
<strong>Legend</strong>
|
|
271
220
|
<div><span class="swatch" style="background:#fff;border-color:#94a3b8"></span> Node</div>
|
|
272
|
-
<div><span class="swatch" style="background:#f5f3ff;border-color:#7c3aed"></span>
|
|
221
|
+
<div><span class="swatch" style="background:#f5f3ff;border-color:#7c3aed"></span> Group</div>
|
|
273
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>
|
|
274
224
|
<div><span class="swatch" style="background:#0f172a;border-color:#0f172a;border-radius:50%"></span> END</div>
|
|
275
225
|
<div>
|
|
276
226
|
<span class="swatch" style="background:#f59e0b;border-color:#f59e0b;height:3px;border-radius:2px"></span>
|
|
277
|
-
Conditional
|
|
227
|
+
Conditional move
|
|
278
228
|
</div>
|
|
279
229
|
</section>
|
|
280
230
|
</aside>
|
|
281
231
|
<main>
|
|
282
232
|
<header id="toolbar">
|
|
283
233
|
<div>
|
|
284
|
-
<button id="back" type="button">← Back</button>
|
|
285
234
|
<h2 id="title">Loading…</h2>
|
|
286
235
|
<div id="subtitle"></div>
|
|
287
236
|
</div>
|
|
@@ -303,10 +252,11 @@
|
|
|
303
252
|
</main>
|
|
304
253
|
<aside id="details">
|
|
305
254
|
<h3>Workflow viewer</h3>
|
|
306
|
-
<p class="muted">Click a node or an edge.
|
|
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>
|
|
307
257
|
<div class="hint">
|
|
308
|
-
The data is generated from <code
|
|
309
|
-
<code>bun run viz</code> after editing
|
|
258
|
+
The data is generated from <code>.next-steps/workflow.json</code>. Run
|
|
259
|
+
<code>bun run viz</code> after editing the graph.
|
|
310
260
|
</div>
|
|
311
261
|
</aside>
|
|
312
262
|
</div>
|
|
@@ -328,14 +278,22 @@
|
|
|
328
278
|
|
|
329
279
|
const $ = (selector) => document.querySelector(selector);
|
|
330
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
|
+
|
|
331
288
|
const COLORS = {
|
|
332
289
|
nodeBg: "#ffffff",
|
|
333
290
|
nodeBorder: "#94a3b8",
|
|
334
291
|
nodeText: "#0f172a",
|
|
335
|
-
|
|
336
|
-
|
|
292
|
+
groupBg: "#f5f3ff",
|
|
293
|
+
groupBorder: "#7c3aed",
|
|
337
294
|
startBg: "#f0fdf4",
|
|
338
295
|
startBorder: "#16a34a",
|
|
296
|
+
reviewBorder: "#f59e0b",
|
|
339
297
|
endBg: "#0f172a",
|
|
340
298
|
endText: "#ffffff",
|
|
341
299
|
edge: "#94a3b8",
|
|
@@ -344,7 +302,7 @@
|
|
|
344
302
|
};
|
|
345
303
|
|
|
346
304
|
// Customise the graph here. Cytoscape selectors support classes,
|
|
347
|
-
// data attributes
|
|
305
|
+
// data attributes, and states (`:selected`).
|
|
348
306
|
const STYLE = [
|
|
349
307
|
{
|
|
350
308
|
selector: "node",
|
|
@@ -357,8 +315,9 @@
|
|
|
357
315
|
height: "label",
|
|
358
316
|
padding: 10,
|
|
359
317
|
label: "data(label)",
|
|
318
|
+
"font-family": FONT_UI,
|
|
360
319
|
color: COLORS.nodeText,
|
|
361
|
-
"font-size":
|
|
320
|
+
"font-size": 12,
|
|
362
321
|
"text-wrap": "wrap",
|
|
363
322
|
"text-max-width": "150px",
|
|
364
323
|
"text-valign": "center",
|
|
@@ -366,11 +325,22 @@
|
|
|
366
325
|
},
|
|
367
326
|
},
|
|
368
327
|
{
|
|
369
|
-
selector: "node.
|
|
328
|
+
selector: "node.group",
|
|
370
329
|
style: {
|
|
371
|
-
"background-color": COLORS.
|
|
372
|
-
"
|
|
373
|
-
|
|
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,
|
|
374
344
|
},
|
|
375
345
|
},
|
|
376
346
|
{
|
|
@@ -381,6 +351,13 @@
|
|
|
381
351
|
"border-width": 3,
|
|
382
352
|
},
|
|
383
353
|
},
|
|
354
|
+
{
|
|
355
|
+
selector: "node.review",
|
|
356
|
+
style: {
|
|
357
|
+
"border-color": COLORS.reviewBorder,
|
|
358
|
+
"border-width": 3,
|
|
359
|
+
},
|
|
360
|
+
},
|
|
384
361
|
{
|
|
385
362
|
selector: "node.end",
|
|
386
363
|
style: {
|
|
@@ -390,7 +367,8 @@
|
|
|
390
367
|
shape: "ellipse",
|
|
391
368
|
width: 52,
|
|
392
369
|
height: 52,
|
|
393
|
-
"font-
|
|
370
|
+
"font-family": FONT_UI,
|
|
371
|
+
"font-size": 12,
|
|
394
372
|
"font-weight": "bold",
|
|
395
373
|
},
|
|
396
374
|
},
|
|
@@ -410,7 +388,8 @@
|
|
|
410
388
|
"target-arrow-shape": "triangle",
|
|
411
389
|
"curve-style": "bezier",
|
|
412
390
|
label: "data(label)",
|
|
413
|
-
"font-
|
|
391
|
+
"font-family": FONT_UI,
|
|
392
|
+
"font-size": 10,
|
|
414
393
|
color: "#475569",
|
|
415
394
|
"text-wrap": "wrap",
|
|
416
395
|
"text-max-width": "140px",
|
|
@@ -437,10 +416,7 @@
|
|
|
437
416
|
},
|
|
438
417
|
},
|
|
439
418
|
];
|
|
440
|
-
|
|
441
419
|
const state = {
|
|
442
|
-
current: null,
|
|
443
|
-
history: [],
|
|
444
420
|
labels: true,
|
|
445
421
|
conditions: true,
|
|
446
422
|
};
|
|
@@ -454,106 +430,81 @@
|
|
|
454
430
|
);
|
|
455
431
|
}
|
|
456
432
|
|
|
457
|
-
function edgeLabel(
|
|
433
|
+
function edgeLabel(move) {
|
|
458
434
|
if (!state.labels) return "";
|
|
459
|
-
const parts = [
|
|
460
|
-
if (state.conditions &&
|
|
435
|
+
const parts = [move.description || ""];
|
|
436
|
+
if (state.conditions && move.condition) parts.push(`when: ${move.condition}`);
|
|
461
437
|
return parts.filter(Boolean).join("\n");
|
|
462
438
|
}
|
|
463
439
|
|
|
464
|
-
function renderList() {
|
|
465
|
-
const list = $("#workflow-list");
|
|
466
|
-
const query = $("#search").value.trim().toLowerCase();
|
|
467
|
-
list.innerHTML = "";
|
|
468
|
-
|
|
469
|
-
for (const id of DATA.order) {
|
|
470
|
-
const workflow = DATA.workflows[id];
|
|
471
|
-
const haystack = `${id} ${workflow.title}`.toLowerCase();
|
|
472
|
-
if (query && !haystack.includes(query)) continue;
|
|
473
|
-
|
|
474
|
-
const button = document.createElement("button");
|
|
475
|
-
button.type = "button";
|
|
476
|
-
button.className = `wf${id === state.current ? " active" : ""}`;
|
|
477
|
-
button.innerHTML = `<strong>${esc(workflow.title)}</strong><small>${esc(id)}</small>`;
|
|
478
|
-
button.addEventListener("click", () => openWorkflow(id));
|
|
479
|
-
list.appendChild(button);
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
$("#count").textContent = `${DATA.order.length} workflows`;
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
function openWorkflow(id, push = true) {
|
|
486
|
-
if (!DATA.workflows[id]) return;
|
|
487
|
-
if (push && state.current && state.current !== id) state.history.push(state.current);
|
|
488
|
-
state.current = id;
|
|
489
|
-
render();
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
function goBack() {
|
|
493
|
-
const previous = state.history.pop();
|
|
494
|
-
if (!previous) return;
|
|
495
|
-
state.current = previous;
|
|
496
|
-
render();
|
|
497
|
-
}
|
|
498
|
-
|
|
499
440
|
function render() {
|
|
500
|
-
const workflow = DATA.
|
|
501
|
-
if (!workflow) return;
|
|
502
|
-
|
|
441
|
+
const workflow = DATA.workflow;
|
|
503
442
|
$("#title").textContent = workflow.title;
|
|
504
443
|
$("#subtitle").textContent = `${workflow.id} · start: ${workflow.start}`;
|
|
505
|
-
$("#
|
|
444
|
+
$("#count").textContent = `${workflow.nodes.length} nodes`;
|
|
506
445
|
document.title = `${workflow.title} · next-steps`;
|
|
507
|
-
|
|
508
|
-
renderList();
|
|
509
446
|
renderGraph(workflow);
|
|
510
447
|
}
|
|
511
448
|
|
|
512
449
|
function renderGraph(workflow) {
|
|
513
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
|
+
}
|
|
514
460
|
|
|
515
461
|
for (const node of workflow.nodes) {
|
|
516
|
-
const classes = [
|
|
462
|
+
const classes = ["node"];
|
|
517
463
|
if (node.id === workflow.start) classes.push("start");
|
|
464
|
+
if (node.reviewRequired) classes.push("review");
|
|
518
465
|
elements.push({
|
|
519
466
|
data: {
|
|
520
467
|
id: node.id,
|
|
521
|
-
label: node.title,
|
|
468
|
+
label: node.reviewRequired ? `${node.title} [review]` : node.title,
|
|
522
469
|
title: node.title,
|
|
523
|
-
type: node.type,
|
|
524
470
|
description: node.description,
|
|
525
|
-
|
|
471
|
+
group: node.group || "",
|
|
472
|
+
reviewRequired: Boolean(node.reviewRequired),
|
|
473
|
+
...(node.group ? { parent: groups.get(node.group) } : {}),
|
|
526
474
|
},
|
|
527
475
|
classes: classes.join(" "),
|
|
528
476
|
});
|
|
529
477
|
}
|
|
530
478
|
|
|
531
|
-
|
|
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")) {
|
|
532
484
|
elements.push({
|
|
533
485
|
data: {
|
|
534
486
|
id: "END",
|
|
535
487
|
label: "END",
|
|
536
488
|
title: "END",
|
|
537
|
-
|
|
538
|
-
description: "This scope is finished.",
|
|
489
|
+
description: "The run is finished.",
|
|
539
490
|
},
|
|
540
491
|
classes: "end",
|
|
541
492
|
});
|
|
542
493
|
}
|
|
543
494
|
|
|
544
|
-
for (const
|
|
495
|
+
for (const move of moves) {
|
|
545
496
|
elements.push({
|
|
546
497
|
data: {
|
|
547
|
-
id:
|
|
548
|
-
source:
|
|
549
|
-
target:
|
|
550
|
-
label: edgeLabel(
|
|
551
|
-
from:
|
|
552
|
-
to:
|
|
553
|
-
description:
|
|
554
|
-
condition:
|
|
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 || "",
|
|
555
506
|
},
|
|
556
|
-
classes:
|
|
507
|
+
classes: move.condition ? "conditional" : "edge",
|
|
557
508
|
});
|
|
558
509
|
}
|
|
559
510
|
|
|
@@ -571,15 +522,12 @@
|
|
|
571
522
|
maxZoom: 2.5,
|
|
572
523
|
});
|
|
573
524
|
|
|
574
|
-
cy.on("tap", "node", (event) => showNode(event.target));
|
|
525
|
+
cy.on("tap", "node.node", (event) => showNode(event.target));
|
|
526
|
+
cy.on("tap", "node.end", (event) => showNode(event.target));
|
|
575
527
|
cy.on("tap", "edge", (event) => showEdge(event.target));
|
|
576
528
|
cy.on("tap", (event) => {
|
|
577
529
|
if (event.target === cy) showIntro();
|
|
578
530
|
});
|
|
579
|
-
cy.on("dblclick", "node", (event) => {
|
|
580
|
-
const workflowId = event.target.data("workflow");
|
|
581
|
-
if (workflowId) openWorkflow(workflowId);
|
|
582
|
-
});
|
|
583
531
|
|
|
584
532
|
cy.ready(() => runLayout());
|
|
585
533
|
showIntro();
|
|
@@ -624,36 +572,33 @@
|
|
|
624
572
|
function showIntro() {
|
|
625
573
|
$("#details").innerHTML = `
|
|
626
574
|
<h3>Workflow viewer</h3>
|
|
627
|
-
<p class="muted">Click a node or an edge.
|
|
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>
|
|
628
577
|
<div class="hint">
|
|
629
|
-
The data is generated from <code
|
|
630
|
-
<code>bun run viz</code> after editing
|
|
578
|
+
The data is generated from <code>.next-steps/workflow.json</code>. Run
|
|
579
|
+
<code>bun run viz</code> after editing the graph.
|
|
631
580
|
</div>
|
|
632
581
|
`;
|
|
633
582
|
}
|
|
634
583
|
|
|
635
584
|
function showNode(node) {
|
|
636
585
|
const data = node.data();
|
|
637
|
-
const
|
|
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
|
+
: "";
|
|
638
590
|
|
|
639
591
|
$("#details").innerHTML = `
|
|
640
592
|
<h3>${esc(data.title)}</h3>
|
|
641
|
-
<div class="muted"><code>${esc(data.id)}</code
|
|
593
|
+
<div class="muted"><code>${esc(data.id)}</code>${data.id === "END" ? " · END" : ""}</div>
|
|
642
594
|
<dl>
|
|
643
595
|
<dt>Description</dt><dd>${esc(data.description || "—")}</dd>
|
|
596
|
+
${group}
|
|
597
|
+
${review}
|
|
644
598
|
</dl>
|
|
645
599
|
`;
|
|
646
600
|
|
|
647
|
-
if (data.
|
|
648
|
-
const button = document.createElement("button");
|
|
649
|
-
button.type = "button";
|
|
650
|
-
button.className = "primary";
|
|
651
|
-
button.textContent = `Open ${data.workflow} →`;
|
|
652
|
-
button.addEventListener("click", () => openWorkflow(data.workflow));
|
|
653
|
-
$("#details").appendChild(button);
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
if (data.type !== "end") {
|
|
601
|
+
if (data.id !== "END") {
|
|
657
602
|
const hint = document.createElement("div");
|
|
658
603
|
hint.className = "hint";
|
|
659
604
|
hint.innerHTML = `Log this step with <code>next-steps log --key <key> --node ${esc(
|
|
@@ -666,14 +611,13 @@
|
|
|
666
611
|
function showEdge(edge) {
|
|
667
612
|
const data = edge.data();
|
|
668
613
|
$("#details").innerHTML = `
|
|
669
|
-
<h3
|
|
670
|
-
<div class="muted"><code>${esc(data.from)}</code> → <code>${esc(data.to)}</code></div>
|
|
614
|
+
<h3>${esc(data.from)} → ${esc(data.to)}</h3>
|
|
671
615
|
<dl>
|
|
672
616
|
<dt>Description</dt><dd>${esc(data.description || "—")}</dd>
|
|
673
617
|
<dt>Condition</dt><dd>${esc(data.condition || "—")}</dd>
|
|
674
618
|
</dl>
|
|
675
619
|
<div class="hint">
|
|
676
|
-
Conditions are prose. The agent decides; the logged
|
|
620
|
+
Conditions are prose. The agent decides; the logged move is the audit trail.
|
|
677
621
|
</div>
|
|
678
622
|
`;
|
|
679
623
|
}
|
|
@@ -682,12 +626,10 @@
|
|
|
682
626
|
if (!cy) return;
|
|
683
627
|
const link = document.createElement("a");
|
|
684
628
|
link.href = cy.png({ full: true, scale: 2, bg: "#ffffff" });
|
|
685
|
-
link.download = `${
|
|
629
|
+
link.download = `${DATA.workflow.id}.png`;
|
|
686
630
|
link.click();
|
|
687
631
|
}
|
|
688
632
|
|
|
689
|
-
$("#search").addEventListener("input", renderList);
|
|
690
|
-
$("#back").addEventListener("click", goBack);
|
|
691
633
|
$("#fit").addEventListener("click", () => cy?.fit(undefined, 30));
|
|
692
634
|
$("#relayout").addEventListener("click", runLayout);
|
|
693
635
|
$("#png").addEventListener("click", exportPng);
|
|
@@ -700,7 +642,6 @@
|
|
|
700
642
|
refreshEdgeLabels();
|
|
701
643
|
});
|
|
702
644
|
|
|
703
|
-
state.current = DATA.workflows["support-ticket"] ? "support-ticket" : DATA.order[0];
|
|
704
645
|
render();
|
|
705
646
|
</script>
|
|
706
647
|
</body>
|