@nanobpm/nano-workforce 0.48.2 → 0.50.0
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/CHANGELOG.md +14 -0
- package/app/plan.ts +12 -0
- package/db/migrations/022_plan_wave_progress.sql +24 -0
- package/package.json +2 -2
- package/pages/epic-detail.page.json +314 -0
- package/pages/epic.page.json +8 -235
- package/pages/home.page.json +219 -47
- package/scripts/pages-contract.test.ts +8 -5
- package/workers/merge/worker.test.ts +112 -0
- package/workers/merge/worker.ts +23 -18
- package/workers/record-plan/worker.test.ts +89 -0
- package/workers/record-plan/worker.ts +8 -0
- package/workers/record-wave/worker.test.ts +56 -0
- package/workers/record-wave/worker.ts +14 -0
- package/workers/select-wave/worker.test.ts +33 -2
- package/workers/select-wave/worker.ts +23 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
# [0.50.0](https://github.com/nanobpm/nano-workforce/compare/v0.49.0...v0.50.0) (2026-08-12)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* **merge:** adopt @nanobpm/urban/effect matchTags for exhaustive land dispatch ([#139](https://github.com/nanobpm/nano-workforce/issues/139)) ([c251763](https://github.com/nanobpm/nano-workforce/commit/c251763a050858ea610ce45e59a34f859163c29c)), closes [nano-ide#215](https://github.com/nano-ide/issues/215)
|
|
7
|
+
|
|
8
|
+
# [0.49.0](https://github.com/nanobpm/nano-workforce/compare/v0.48.2...v0.49.0) (2026-08-12)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* **ui:** per-epic operator workspace with live wave progress ([#138](https://github.com/nanobpm/nano-workforce/issues/138)) ([127888d](https://github.com/nanobpm/nano-workforce/commit/127888d5d2b66e1e9d1c85d015cbd65f3696216f)), closes [#137](https://github.com/nanobpm/nano-workforce/issues/137) [nanobpm/nano-workforce#137](https://github.com/nanobpm/nano-workforce/issues/137) [nanobpm/nano-ide#213](https://github.com/nanobpm/nano-ide/issues/213) [nanobpm/nano-ide#214](https://github.com/nanobpm/nano-ide/issues/214)
|
|
14
|
+
|
|
1
15
|
## [0.48.2](https://github.com/nanobpm/nano-workforce/compare/v0.48.1...v0.48.2) (2026-08-12)
|
|
2
16
|
|
|
3
17
|
|
package/app/plan.ts
CHANGED
|
@@ -55,6 +55,18 @@ export interface Plan {
|
|
|
55
55
|
// Wave-merge barrier (007_wave_gate.sql): the wave index whose PRs the plan is currently
|
|
56
56
|
// waiting to see MERGED before dispatching the next wave, or null when not parked at the barrier.
|
|
57
57
|
gate_wave: number | null;
|
|
58
|
+
// Operator-visibility wave progress (022_plan_wave_progress.sql, #137): denormalised so the
|
|
59
|
+
// epics-index can show wave X/N at a glance. `wave_count` is the total waves (N); `current_wave`
|
|
60
|
+
// is the 0-based index of the wave the fleet is actively implementing (advanced by select-wave,
|
|
61
|
+
// pinned to wave_count-1 on completion). Display-only — never gates control flow. NULL until the
|
|
62
|
+
// plan is dispatched with tasks.
|
|
63
|
+
wave_count: number | null;
|
|
64
|
+
current_wave: number | null;
|
|
65
|
+
// Pre-formatted 1-based "X/N" progress string for the epics-index at-a-glance column
|
|
66
|
+
// (022_plan_wave_progress.sql, #137). The dataGrid has no per-cell templating (nano-ide#214),
|
|
67
|
+
// so this is projected alongside the numeric columns by the same worker writes. NULL until
|
|
68
|
+
// dispatched with tasks.
|
|
69
|
+
wave_label: string | null;
|
|
58
70
|
// Per-plan capability token for the coordination blackboard (009_plan_blackboard.sql, #51).
|
|
59
71
|
// Minted at plan start; baked into the blackboard URL handed to implementer agents. NULL for
|
|
60
72
|
// plans created before the blackboard shipped.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
-- Operator visibility: at-a-glance wave X/N on the Epic Coordination view (issue #137).
|
|
2
|
+
--
|
|
3
|
+
-- `plan_tasks.wave` (005_plan_deps.sql) holds each task's 0-based wave, but the `plans`
|
|
4
|
+
-- row itself carries no progress summary — so the epics-index grid had no way to show how
|
|
5
|
+
-- far through its waves an epic is without joining/aggregating per row. Urban's datasource
|
|
6
|
+
-- cannot read a SQL VIEW (gateway.ts schema() whitelists only type='table'), so following
|
|
7
|
+
-- the codebase's existing convention (denormalised, worker-maintained pointers on `plans`
|
|
8
|
+
-- like open_task_id / open_plan_round / gate_wave) we project two flat columns:
|
|
9
|
+
--
|
|
10
|
+
-- • wave_count — total number of waves (N). Written by `record-plan` alongside
|
|
11
|
+
-- task_count, from the `waveCount` it already computes (app/waves.ts).
|
|
12
|
+
-- • current_wave — the 0-based index of the wave the fleet is ACTIVELY implementing
|
|
13
|
+
-- (live). Initialised to 0 by `record-plan` at dispatch, advanced by
|
|
14
|
+
-- `select-wave` at the start of each wave, and pinned to wave_count-1 on
|
|
15
|
+
-- completion so a finished epic reads N/N. Display-only: it must never
|
|
16
|
+
-- gate control flow (that stays driven by the currentWave/waveCount/
|
|
17
|
+
-- gate_wave process state). NULL until the plan is dispatched with tasks.
|
|
18
|
+
-- • wave_label — the human "X/N" progress string (1-based), e.g. "2/3". The dataGrid
|
|
19
|
+
-- renders raw field values with no per-cell templating, so this pre-formats
|
|
20
|
+
-- (current_wave+1)/wave_count for the epics-index at-a-glance column. Kept in
|
|
21
|
+
-- lockstep with the two numeric columns by the same worker writes.
|
|
22
|
+
ALTER TABLE plans ADD COLUMN wave_count INTEGER;
|
|
23
|
+
ALTER TABLE plans ADD COLUMN current_wave INTEGER;
|
|
24
|
+
ALTER TABLE plans ADD COLUMN wave_label TEXT;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nanobpm/nano-workforce",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.50.0",
|
|
4
4
|
"description": "Nano Workforce — an Agent Graph Orchestration application for Agentic SDLC: durable BPMN processes that coordinate a graph of AI agents across the software delivery lifecycle.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "main.ts",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"lint:fix": "biome check --write app operations workers pages components scripts e2e main.ts"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@nanobpm/urban": "^0.
|
|
49
|
+
"@nanobpm/urban": "^0.45.0"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"@biomejs/biome": "^2.4.11",
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": "1.0",
|
|
3
|
+
"title": "Epic",
|
|
4
|
+
"nodes": [
|
|
5
|
+
{
|
|
6
|
+
"type": "nav",
|
|
7
|
+
"id": "nav",
|
|
8
|
+
"props": {
|
|
9
|
+
"variant": "bar",
|
|
10
|
+
"sticky": true,
|
|
11
|
+
"title": "Nano Workforce",
|
|
12
|
+
"items": [
|
|
13
|
+
{ "label": "Convergence", "page": "home" },
|
|
14
|
+
{ "label": "Epics", "page": "epic" }
|
|
15
|
+
]
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"type": "text",
|
|
20
|
+
"id": "title",
|
|
21
|
+
"props": { "text": "Epic {{param}}", "variant": "heading" }
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"type": "text",
|
|
25
|
+
"id": "subtitle",
|
|
26
|
+
"props": {
|
|
27
|
+
"text": "This epic's plan, review trace, wave state, conflict graph, coordination notes, and trial-merge results. Sections start collapsed and remember your choice.",
|
|
28
|
+
"variant": "sub"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"type": "dataGrid",
|
|
33
|
+
"id": "epic-plan",
|
|
34
|
+
"props": {
|
|
35
|
+
"title": "Plan",
|
|
36
|
+
"rowKey": "plan_key",
|
|
37
|
+
"refreshMs": 5000,
|
|
38
|
+
"data": {
|
|
39
|
+
"kind": "datasource",
|
|
40
|
+
"source": "app",
|
|
41
|
+
"table": "plans",
|
|
42
|
+
"orderBy": { "field": "updated_at", "dir": "desc" },
|
|
43
|
+
"filter": [{ "field": "plan_key", "eqParam": true }]
|
|
44
|
+
},
|
|
45
|
+
"columns": [
|
|
46
|
+
{ "field": "plan_key", "header": "Issue", "linkField": "issue_url" },
|
|
47
|
+
{ "field": "status", "header": "Status", "link": { "kind": "processExplorer", "keyField": "process_key" } },
|
|
48
|
+
{ "field": "wave_label", "header": "Wave" },
|
|
49
|
+
{ "field": "task_count", "header": "Tasks" },
|
|
50
|
+
{ "field": "open_task_id", "header": "Open escalation" },
|
|
51
|
+
{ "field": "open_plan_round", "header": "Plan escalation round" },
|
|
52
|
+
{ "field": "updated_at", "header": "Updated" }
|
|
53
|
+
],
|
|
54
|
+
"rowActions": [
|
|
55
|
+
{
|
|
56
|
+
"label": "Proceed with plan",
|
|
57
|
+
"confirm": "Dispatch the current unapproved plan as a human override?",
|
|
58
|
+
"showWhenField": "open_plan_escalation_id",
|
|
59
|
+
"action": {
|
|
60
|
+
"path": "/app/api/actions/message",
|
|
61
|
+
"body": {
|
|
62
|
+
"name": "plan-escalation-answered",
|
|
63
|
+
"correlationKey": "{{row.plan_key}}",
|
|
64
|
+
"variables": { "directive": "proceed", "note": "Proceed override from the epic page." }
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
],
|
|
69
|
+
"detail": {
|
|
70
|
+
"linkField": "issue_url",
|
|
71
|
+
"fields": [
|
|
72
|
+
{ "field": "repo", "label": "Repository" },
|
|
73
|
+
{ "field": "issue_number", "label": "Issue number" },
|
|
74
|
+
{ "field": "base_branch", "label": "Base branch (blank = repo default)" },
|
|
75
|
+
{ "field": "outcome", "label": "Outcome" },
|
|
76
|
+
{ "field": "open_plan_findings", "label": "Open plan-review findings" },
|
|
77
|
+
{ "field": "open_task_question", "label": "Open escalation question" }
|
|
78
|
+
],
|
|
79
|
+
"form": {
|
|
80
|
+
"showWhenField": "open_plan_escalation_id",
|
|
81
|
+
"title": "Answer the open plan-review escalation",
|
|
82
|
+
"promptField": "open_plan_findings",
|
|
83
|
+
"inputKey": "note",
|
|
84
|
+
"inputLabel": "Revision directive note for the planner",
|
|
85
|
+
"submitLabel": "Revise plan",
|
|
86
|
+
"action": {
|
|
87
|
+
"path": "/app/api/actions/message",
|
|
88
|
+
"body": {
|
|
89
|
+
"name": "plan-escalation-answered",
|
|
90
|
+
"correlationKey": "{{row.plan_key}}",
|
|
91
|
+
"variables": { "directive": "revise", "note": "{{form.note}}" }
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"type": "dataGrid",
|
|
100
|
+
"id": "wave-state",
|
|
101
|
+
"props": {
|
|
102
|
+
"title": "Wave state",
|
|
103
|
+
"rowKey": "id",
|
|
104
|
+
"refreshMs": 5000,
|
|
105
|
+
"collapsible": true,
|
|
106
|
+
"defaultCollapsed": true,
|
|
107
|
+
"groupBy": "wave",
|
|
108
|
+
"groupLabel": "Wave",
|
|
109
|
+
"data": {
|
|
110
|
+
"kind": "datasource",
|
|
111
|
+
"source": "app",
|
|
112
|
+
"table": "plan_tasks",
|
|
113
|
+
"orderBy": { "field": "wave", "dir": "asc" },
|
|
114
|
+
"filter": [
|
|
115
|
+
{ "field": "plan_key", "eqParam": true },
|
|
116
|
+
{ "field": "status", "in": ["pending", "opened", "escalated", "waiting-for-lane", "blocked"] }
|
|
117
|
+
]
|
|
118
|
+
},
|
|
119
|
+
"tabs": [
|
|
120
|
+
{
|
|
121
|
+
"label": "Active",
|
|
122
|
+
"filter": [
|
|
123
|
+
{ "field": "plan_key", "eqParam": true },
|
|
124
|
+
{ "field": "status", "in": ["pending", "opened", "escalated", "waiting-for-lane", "blocked"] }
|
|
125
|
+
]
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
"label": "Skipped",
|
|
129
|
+
"filter": [
|
|
130
|
+
{ "field": "plan_key", "eqParam": true },
|
|
131
|
+
{ "field": "status", "in": ["skipped"] }
|
|
132
|
+
]
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
"label": "All",
|
|
136
|
+
"filter": [{ "field": "plan_key", "eqParam": true }]
|
|
137
|
+
}
|
|
138
|
+
],
|
|
139
|
+
"columns": [
|
|
140
|
+
{ "field": "wave", "header": "Wave" },
|
|
141
|
+
{ "field": "task_id", "header": "Task" },
|
|
142
|
+
{ "field": "title", "header": "Title" },
|
|
143
|
+
{ "field": "status", "header": "Status" },
|
|
144
|
+
{ "field": "pr_key", "header": "PR" },
|
|
145
|
+
{ "field": "summary", "header": "Summary" },
|
|
146
|
+
{ "field": "updated_at", "header": "Updated" }
|
|
147
|
+
],
|
|
148
|
+
"detail": {
|
|
149
|
+
"fields": [
|
|
150
|
+
{ "field": "open_question", "label": "Open question" },
|
|
151
|
+
{ "field": "answer", "label": "Answer" },
|
|
152
|
+
{ "field": "draft_pr_key", "label": "Draft PR" },
|
|
153
|
+
{ "field": "prompt", "label": "Prompt" }
|
|
154
|
+
]
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
"type": "dataGrid",
|
|
160
|
+
"id": "plan-reviews",
|
|
161
|
+
"props": {
|
|
162
|
+
"title": "Plan review trace",
|
|
163
|
+
"refreshMs": 5000,
|
|
164
|
+
"collapsible": true,
|
|
165
|
+
"defaultCollapsed": true,
|
|
166
|
+
"data": {
|
|
167
|
+
"kind": "datasource",
|
|
168
|
+
"source": "app",
|
|
169
|
+
"table": "plan_reviews",
|
|
170
|
+
"orderBy": { "field": "round", "dir": "asc" },
|
|
171
|
+
"filter": [{ "field": "plan_key", "eqParam": true }]
|
|
172
|
+
},
|
|
173
|
+
"columns": [
|
|
174
|
+
{ "field": "round", "header": "Round" },
|
|
175
|
+
{ "field": "epoch", "header": "Epoch" },
|
|
176
|
+
{ "field": "approved", "header": "Approved? (1/0)" },
|
|
177
|
+
{ "field": "findings", "header": "Reviewer findings" },
|
|
178
|
+
{ "field": "created_at", "header": "Recorded" }
|
|
179
|
+
]
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
"type": "dataGrid",
|
|
184
|
+
"id": "plan-review-escalations",
|
|
185
|
+
"props": {
|
|
186
|
+
"title": "Plan-review escalations",
|
|
187
|
+
"rowKey": "id",
|
|
188
|
+
"refreshMs": 5000,
|
|
189
|
+
"collapsible": true,
|
|
190
|
+
"defaultCollapsed": true,
|
|
191
|
+
"data": {
|
|
192
|
+
"kind": "datasource",
|
|
193
|
+
"source": "app",
|
|
194
|
+
"table": "plan_review_escalations",
|
|
195
|
+
"orderBy": { "field": "id", "dir": "desc" },
|
|
196
|
+
"filter": [{ "field": "plan_key", "eqParam": true }]
|
|
197
|
+
},
|
|
198
|
+
"columns": [
|
|
199
|
+
{ "field": "epoch", "header": "Epoch" },
|
|
200
|
+
{ "field": "round", "header": "Round" },
|
|
201
|
+
{ "field": "findings", "header": "Findings" },
|
|
202
|
+
{ "field": "status", "header": "Status" },
|
|
203
|
+
{ "field": "directive", "header": "Directive" },
|
|
204
|
+
{ "field": "note", "header": "Human note" },
|
|
205
|
+
{ "field": "asked_at", "header": "Asked" },
|
|
206
|
+
{ "field": "answered_at", "header": "Answered" }
|
|
207
|
+
]
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
"type": "dataGrid",
|
|
212
|
+
"id": "merge-exclusions",
|
|
213
|
+
"props": {
|
|
214
|
+
"title": "Conflict graph / who blocks whom",
|
|
215
|
+
"rowKey": "id",
|
|
216
|
+
"refreshMs": 5000,
|
|
217
|
+
"collapsible": true,
|
|
218
|
+
"defaultCollapsed": true,
|
|
219
|
+
"data": {
|
|
220
|
+
"kind": "datasource",
|
|
221
|
+
"source": "app",
|
|
222
|
+
"table": "plan_merge_exclusions",
|
|
223
|
+
"orderBy": { "field": "id", "dir": "asc" },
|
|
224
|
+
"filter": [{ "field": "plan_key", "eqParam": true }]
|
|
225
|
+
},
|
|
226
|
+
"columns": [
|
|
227
|
+
{ "field": "task_a", "header": "Task A" },
|
|
228
|
+
{ "field": "task_b", "header": "Task B" },
|
|
229
|
+
{ "field": "files", "header": "Shared files" },
|
|
230
|
+
{ "field": "source", "header": "Source" },
|
|
231
|
+
{ "field": "updated_at", "header": "Updated" }
|
|
232
|
+
]
|
|
233
|
+
}
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
"type": "dataGrid",
|
|
237
|
+
"id": "coordination-notes",
|
|
238
|
+
"props": {
|
|
239
|
+
"title": "Coordination notes",
|
|
240
|
+
"rowKey": "id",
|
|
241
|
+
"refreshMs": 5000,
|
|
242
|
+
"collapsible": true,
|
|
243
|
+
"defaultCollapsed": true,
|
|
244
|
+
"data": {
|
|
245
|
+
"kind": "datasource",
|
|
246
|
+
"source": "app",
|
|
247
|
+
"table": "plan_blackboard",
|
|
248
|
+
"orderBy": { "field": "id", "dir": "asc" },
|
|
249
|
+
"filter": [{ "field": "plan_key", "eqParam": true }]
|
|
250
|
+
},
|
|
251
|
+
"columns": [
|
|
252
|
+
{ "field": "wave", "header": "Wave" },
|
|
253
|
+
{ "field": "author_task", "header": "Agent" },
|
|
254
|
+
{ "field": "kind", "header": "Kind" },
|
|
255
|
+
{ "field": "files", "header": "Files" },
|
|
256
|
+
{ "field": "body", "header": "Note" },
|
|
257
|
+
{ "field": "created_at", "header": "Posted" }
|
|
258
|
+
]
|
|
259
|
+
}
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
"type": "dataGrid",
|
|
263
|
+
"id": "trial-merge-results",
|
|
264
|
+
"props": {
|
|
265
|
+
"title": "Trial-merge results",
|
|
266
|
+
"rowKey": "id",
|
|
267
|
+
"refreshMs": 5000,
|
|
268
|
+
"collapsible": true,
|
|
269
|
+
"defaultCollapsed": true,
|
|
270
|
+
"data": {
|
|
271
|
+
"kind": "datasource",
|
|
272
|
+
"source": "app",
|
|
273
|
+
"table": "plan_trial_merges",
|
|
274
|
+
"orderBy": { "field": "created_at", "dir": "desc" },
|
|
275
|
+
"filter": [
|
|
276
|
+
{ "field": "plan_key", "eqParam": true },
|
|
277
|
+
{ "field": "result", "in": ["merge-conflict", "suite-failed"] },
|
|
278
|
+
{ "field": "resolved", "in": [0] }
|
|
279
|
+
]
|
|
280
|
+
},
|
|
281
|
+
"tabs": [
|
|
282
|
+
{
|
|
283
|
+
"label": "Needs attention",
|
|
284
|
+
"filter": [
|
|
285
|
+
{ "field": "plan_key", "eqParam": true },
|
|
286
|
+
{ "field": "result", "in": ["merge-conflict", "suite-failed"] },
|
|
287
|
+
{ "field": "resolved", "in": [0] }
|
|
288
|
+
]
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
"label": "Clean",
|
|
292
|
+
"filter": [
|
|
293
|
+
{ "field": "plan_key", "eqParam": true },
|
|
294
|
+
{ "field": "result", "in": ["clean"] }
|
|
295
|
+
]
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
"label": "All",
|
|
299
|
+
"filter": [{ "field": "plan_key", "eqParam": true }]
|
|
300
|
+
}
|
|
301
|
+
],
|
|
302
|
+
"columns": [
|
|
303
|
+
{ "field": "wave", "header": "Wave" },
|
|
304
|
+
{ "field": "result", "header": "Result" },
|
|
305
|
+
{ "field": "heads", "header": "Heads" },
|
|
306
|
+
{ "field": "conflicts", "header": "Conflicts" },
|
|
307
|
+
{ "field": "failing", "header": "Failing" },
|
|
308
|
+
{ "field": "summary", "header": "Summary" },
|
|
309
|
+
{ "field": "created_at", "header": "Recorded" }
|
|
310
|
+
]
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
]
|
|
314
|
+
}
|