@nanobpm/nano-workforce 0.83.0 → 0.84.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 CHANGED
@@ -1,3 +1,10 @@
1
+ # [0.84.0](https://github.com/nanobpm/nano-workforce/compare/v0.83.0...v0.84.0) (2026-08-17)
2
+
3
+
4
+ ### Features
5
+
6
+ * **ui:** render narrative epic-detail sections with urban prose renderer ([#270](https://github.com/nanobpm/nano-workforce/issues/270)) ([#271](https://github.com/nanobpm/nano-workforce/issues/271)) ([38c67a4](https://github.com/nanobpm/nano-workforce/commit/38c67a479693730b9a79f3a7b0f88ef574d3a456)), closes [nano-ide#274](https://github.com/nano-ide/issues/274) [#87](https://github.com/nanobpm/nano-workforce/issues/87) [274/#275](https://github.com/nanobpm/nano-workforce/issues/275)
7
+
1
8
  # [0.83.0](https://github.com/nanobpm/nano-workforce/compare/v0.82.1...v0.83.0) (2026-08-17)
2
9
 
3
10
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nanobpm/nano-workforce",
3
- "version": "0.83.0",
3
+ "version": "0.84.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",
@@ -53,7 +53,7 @@
53
53
  },
54
54
  "dependencies": {
55
55
  "@nanobpm/agentic": "^0.1.0",
56
- "@nanobpm/urban": "^0.53.0"
56
+ "@nanobpm/urban": "^0.54.0"
57
57
  },
58
58
  "devDependencies": {
59
59
  "@biomejs/biome": "^2.4.11",
@@ -137,38 +137,36 @@
137
137
  }
138
138
  },
139
139
  {
140
- "type": "dataGrid",
140
+ "type": "prose",
141
141
  "id": "plan-reviews",
142
142
  "props": {
143
143
  "title": "Plan review trace",
144
144
  "refreshMs": 5000,
145
145
  "collapsible": true,
146
146
  "defaultCollapsed": true,
147
+ "measure": 80,
148
+ "empty": "No plan reviews recorded yet.",
147
149
  "data": {
148
150
  "kind": "datasource",
149
151
  "source": "app",
150
152
  "table": "plan_reviews",
151
- "orderBy": { "field": "round", "dir": "asc" },
153
+ "orderBy": { "field": "created_at", "dir": "asc" },
152
154
  "filter": [{ "field": "plan_key", "eqParam": true }]
153
155
  },
154
- "columns": [
155
- { "field": "round", "header": "Round" },
156
- { "field": "epoch", "header": "Epoch" },
157
- { "field": "approved", "header": "Approved? (1/0)" },
158
- { "field": "findings", "header": "Reviewer findings" },
159
- { "field": "created_at", "header": "Recorded" }
160
- ]
156
+ "header": "Round {{round}} · epoch {{epoch}} · approved {{approved}} · {{created_at}}",
157
+ "body": "findings"
161
158
  }
162
159
  },
163
160
  {
164
- "type": "dataGrid",
161
+ "type": "prose",
165
162
  "id": "plan-review-escalations",
166
163
  "props": {
167
164
  "title": "Plan-review escalations",
168
- "rowKey": "id",
169
165
  "refreshMs": 5000,
170
166
  "collapsible": true,
171
167
  "defaultCollapsed": true,
168
+ "measure": 80,
169
+ "empty": "No plan-review escalations.",
172
170
  "data": {
173
171
  "kind": "datasource",
174
172
  "source": "app",
@@ -176,16 +174,8 @@
176
174
  "orderBy": { "field": "id", "dir": "desc" },
177
175
  "filter": [{ "field": "plan_key", "eqParam": true }]
178
176
  },
179
- "columns": [
180
- { "field": "epoch", "header": "Epoch" },
181
- { "field": "round", "header": "Round" },
182
- { "field": "findings", "header": "Findings" },
183
- { "field": "status", "header": "Status" },
184
- { "field": "directive", "header": "Directive" },
185
- { "field": "note", "header": "Human note" },
186
- { "field": "asked_at", "header": "Asked" },
187
- { "field": "answered_at", "header": "Answered" }
188
- ]
177
+ "header": "Round {{round}} · epoch {{epoch}} · {{status}} · directive {{directive}} · asked {{asked_at}} · answered {{answered_at}} · note {{note}}",
178
+ "body": "findings"
189
179
  }
190
180
  },
191
181
  {
@@ -214,14 +204,15 @@
214
204
  }
215
205
  },
216
206
  {
217
- "type": "dataGrid",
207
+ "type": "prose",
218
208
  "id": "coordination-notes",
219
209
  "props": {
220
210
  "title": "Coordination notes",
221
- "rowKey": "id",
222
211
  "refreshMs": 5000,
223
212
  "collapsible": true,
224
213
  "defaultCollapsed": true,
214
+ "measure": 80,
215
+ "empty": "No coordination notes posted yet.",
225
216
  "data": {
226
217
  "kind": "datasource",
227
218
  "source": "app",
@@ -229,14 +220,8 @@
229
220
  "orderBy": { "field": "id", "dir": "asc" },
230
221
  "filter": [{ "field": "plan_key", "eqParam": true }]
231
222
  },
232
- "columns": [
233
- { "field": "wave", "header": "Wave" },
234
- { "field": "author_task", "header": "Agent" },
235
- { "field": "kind", "header": "Kind" },
236
- { "field": "files", "header": "Files" },
237
- { "field": "body", "header": "Note" },
238
- { "field": "created_at", "header": "Posted" }
239
- ]
223
+ "header": "Wave {{wave}} · {{author_task}} · {{kind}} · files {{files}} · {{created_at}}",
224
+ "body": "body"
240
225
  }
241
226
  },
242
227
  {
@@ -112,6 +112,12 @@ function filterFields(filter: Json): string[] {
112
112
  return filter.map((f: Json) => f?.field).filter(Boolean);
113
113
  }
114
114
 
115
+ // Pull `{{field}}` interpolation names out of a prose renderer's header template (nano-ide#274).
116
+ function templateFields(tpl: Json): string[] {
117
+ if (typeof tpl !== "string") return [];
118
+ return [...tpl.matchAll(/\{\{([^{}]+)\}\}/g)].map((m) => m[1].trim()).filter(Boolean);
119
+ }
120
+
115
121
  function collectRefs(page: string, node: Json, out: Ref[]): void {
116
122
  if (Array.isArray(node)) {
117
123
  for (const v of node) collectRefs(page, v, out);
@@ -120,10 +126,17 @@ function collectRefs(page: string, node: Json, out: Ref[]): void {
120
126
  if (!node || typeof node !== "object") return;
121
127
 
122
128
  // Top-level datasource grid: the datasource lives at `node.data`, while `columns`, `rowKey`,
123
- // `filter`/`tabs`, and `detail` are siblings on the same `node` (the grid props).
129
+ // `filter`/`tabs`, and `detail` are siblings on the same `node` (the grid props). A `prose`
130
+ // renderer (nano-ide#274) binds the same `node.data` but has no `columns`: its displayed content
131
+ // is the header template's `{{field}}` refs plus the single `body` field, so fold those in as
132
+ // "columns" so the field-existence + surface guards below apply to prose sections too.
124
133
  const data = node.data;
125
134
  if (data && data.kind === "datasource" && typeof data.table === "string") {
126
- const columns: string[] = (node.columns ?? []).map((c: Json) => c.field).filter(Boolean);
135
+ const columns: string[] = [
136
+ ...(node.columns ?? []).map((c: Json) => c.field),
137
+ ...templateFields(node.header),
138
+ ...(typeof node.body === "string" ? [node.body] : []),
139
+ ].filter(Boolean);
127
140
  // Every reference that resolves to a column on this table — the runtime 400s on any of them if
128
141
  // it names a column the migrations never created, so all must be guarded, not just displayed
129
142
  // columns. `detail.fields`/`detail.linkField` render columns of the same top-level row.
@@ -215,17 +228,19 @@ test("issue #87: plan_reviews is surfaced on the per-epic detail page", async ()
215
228
  const onEpicDetail = refs.some(
216
229
  (r) => r.page === "epic-detail.page.json" && r.table === "plan_reviews",
217
230
  );
218
- assert(onEpicDetail, "epic-detail.page.json must bind a grid to plan_reviews (plan-review trace)");
231
+ assert(onEpicDetail, "epic-detail.page.json must bind the plan-review trace to plan_reviews");
219
232
 
220
- // The trace is only useful with the verdict + critique columns, so pin them. Assert against the
221
- // visibly displayed `columns` (not `fields`, which also holds binding refs like orderBy.field) so
222
- // a column silently dropped from the grid UI can't pass by being referenced elsewhere.
233
+ // The trace is only useful with the verdict + critique surfaced, so pin them. Assert against the
234
+ // visibly displayed `columns` (not `fields`, which also holds binding refs like orderBy.field)
235
+ // for the `prose` renderer (nano-ide#274) these are the header template's `{{round}}`/`{{approved}}`
236
+ // refs plus the `findings` body — so a field silently dropped from the UI can't pass by being
237
+ // referenced elsewhere.
223
238
  const required = ["round", "approved", "findings"];
224
239
  for (const r of refs.filter((x) => x.table === "plan_reviews")) {
225
240
  for (const col of required) {
226
241
  assert(
227
242
  r.columns.includes(col),
228
- `${r.page}: plan_reviews grid must expose the "${col}" column`,
243
+ `${r.page}: plan_reviews trace must surface the "${col}" field`,
229
244
  );
230
245
  }
231
246
  }