@nanobpm/nano-workforce 0.71.0 → 0.72.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.
@@ -7,28 +7,48 @@
7
7
  "id": "nav",
8
8
  "props": {
9
9
  "variant": "bar",
10
- "sticky": true,
11
10
  "title": "Nano Workforce",
12
11
  "items": [
13
- { "label": "Overview", "page": "overview" },
14
- { "label": "Convergence", "page": "home" },
15
- { "label": "Epics", "page": "epic" },
16
- { "label": "Feature", "page": "feature" },
17
- { "label": "Tasks", "page": "tasks" },
18
- { "label": "Cockpit", "page": "cockpit" }
12
+ {
13
+ "label": "Overview",
14
+ "page": "overview"
15
+ },
16
+ {
17
+ "label": "Convergence",
18
+ "page": "home"
19
+ },
20
+ {
21
+ "label": "Epics",
22
+ "page": "epic"
23
+ },
24
+ {
25
+ "label": "Feature",
26
+ "page": "feature"
27
+ },
28
+ {
29
+ "label": "Tasks",
30
+ "page": "tasks"
31
+ },
32
+ {
33
+ "label": "Cockpit",
34
+ "page": "cockpit"
35
+ }
19
36
  ]
20
37
  }
21
38
  },
22
39
  {
23
40
  "type": "text",
24
41
  "id": "title",
25
- "props": { "text": "Tasks", "variant": "heading" }
42
+ "props": {
43
+ "text": "Tasks",
44
+ "variant": "heading"
45
+ }
26
46
  },
27
47
  {
28
48
  "type": "text",
29
49
  "id": "subtitle",
30
50
  "props": {
31
- "text": "Open native user-task escalations awaiting a human decision \u2014 across feature runs, epics, and PR review loops. Each row parks a process on your decision; submit one and the process resumes on the same canonical path Urban's task inbox uses. Rows disappear once completed (here, via the inbox, or out-of-band).",
51
+ "text": "Open native user-task escalations awaiting a human decision across feature runs, epics, and PR review loops. Each row parks a process on your decision; submit one and the process resumes on the same canonical path Urban's task inbox uses. Rows disappear once completed (here, via the inbox, or out-of-band).",
32
52
  "variant": "sub"
33
53
  }
34
54
  },
@@ -37,57 +57,56 @@
37
57
  "id": "feature-escalations",
38
58
  "props": {
39
59
  "title": "Feature escalations",
40
- "collapsible": true,
41
- "defaultCollapsed": false,
42
- "showCount": true,
43
- "rowKey": "user_task_key",
44
- "refreshMs": 5000,
45
60
  "data": {
46
61
  "kind": "datasource",
47
62
  "source": "app",
48
63
  "table": "user_tasks",
49
- "orderBy": { "field": "updated_at", "dir": "desc" },
50
- "filter": [{ "field": "element_id", "in": ["feature-escalation"] }]
64
+ "filter": [
65
+ {
66
+ "field": "element_id",
67
+ "in": [
68
+ "feature-escalation"
69
+ ]
70
+ }
71
+ ],
72
+ "orderBy": {
73
+ "field": "updated_at",
74
+ "dir": "desc"
75
+ }
51
76
  },
52
77
  "columns": [
53
- { "field": "subject_key", "header": "Feature", "link": { "kind": "page", "page": "feature", "keyField": "subject_key" } },
54
- { "field": "question", "header": "Question" },
55
- { "field": "subject_url", "header": "Issue", "linkField": "subject_url" },
56
- { "field": "process_key", "header": "Process", "link": { "kind": "processExplorer", "keyField": "process_key" } },
57
- { "field": "updated_at", "header": "Updated" }
58
- ],
59
- "rowActions": [
60
78
  {
61
- "label": "Abandon",
62
- "confirm": "Abandon this escalated task? The run gives up on it (no PR).",
63
- "showWhenField": "user_task_key",
64
- "action": {
65
- "path": "/app/api/actions/answer-escalation",
66
- "body": { "userTaskKey": "{{row.user_task_key}}", "resolution": "abandon" }
79
+ "field": "subject_key",
80
+ "header": "Feature"
81
+ },
82
+ {
83
+ "field": "subject_url",
84
+ "header": "Issue"
85
+ },
86
+ {
87
+ "field": "process_key",
88
+ "header": "Process",
89
+ "link": {
90
+ "kind": "processExplorer",
91
+ "keyField": "process_key"
67
92
  }
93
+ },
94
+ {
95
+ "field": "updated_at",
96
+ "header": "Updated"
68
97
  }
69
98
  ],
99
+ "rowKey": "user_task_key",
70
100
  "detail": {
71
101
  "linkField": "subject_url",
72
- "fields": [{ "field": "question", "label": "Escalation question" }],
73
- "form": {
74
- "showWhenField": "user_task_key",
75
- "title": "Answer escalation",
76
- "promptField": "question",
77
- "inputKey": "answer",
78
- "inputLabel": "Guidance for the implementation agent",
79
- "submitLabel": "Answer & re-dispatch",
80
- "action": {
81
- "path": "/app/api/actions/answer-escalation",
82
- "successLabel": "Answered \u2014 the agent will resume",
83
- "body": {
84
- "userTaskKey": "{{row.user_task_key}}",
85
- "resolution": "answer",
86
- "answer": "{{form.answer}}"
87
- }
102
+ "fields": [
103
+ {
104
+ "field": "question",
105
+ "label": "Escalation question"
88
106
  }
89
- }
90
- }
107
+ ]
108
+ },
109
+ "refreshMs": 5000
91
110
  }
92
111
  },
93
112
  {
@@ -95,56 +114,60 @@
95
114
  "id": "plan-reviews",
96
115
  "props": {
97
116
  "title": "Plan-review decisions",
98
- "collapsible": true,
99
- "defaultCollapsed": false,
100
- "showCount": true,
101
- "rowKey": "user_task_key",
102
- "refreshMs": 5000,
103
117
  "data": {
104
118
  "kind": "datasource",
105
119
  "source": "app",
106
120
  "table": "user_tasks",
107
- "orderBy": { "field": "updated_at", "dir": "desc" },
108
- "filter": [{ "field": "element_id", "in": ["plan-review-decision"] }]
121
+ "filter": [
122
+ {
123
+ "field": "element_id",
124
+ "in": [
125
+ "plan-review-decision"
126
+ ]
127
+ }
128
+ ],
129
+ "orderBy": {
130
+ "field": "updated_at",
131
+ "dir": "desc"
132
+ }
109
133
  },
110
134
  "columns": [
111
- { "field": "subject_key", "header": "Epic", "link": { "kind": "page", "page": "epic-detail", "keyField": "subject_key" } },
112
- { "field": "question", "header": "Findings" },
113
- { "field": "subject_url", "header": "Issue", "linkField": "subject_url" },
114
- { "field": "process_key", "header": "Process", "link": { "kind": "processExplorer", "keyField": "process_key" } },
115
- { "field": "updated_at", "header": "Updated" }
116
- ],
117
- "rowActions": [
118
135
  {
119
- "label": "Proceed",
120
- "confirm": "Proceed \u2014 dispatch the current plan as-is?",
121
- "showWhenField": "user_task_key",
122
- "action": {
123
- "path": "/app/api/actions/complete-user-task",
124
- "body": { "userTaskKey": "{{row.user_task_key}}", "variables": { "directive": "proceed" } }
136
+ "field": "subject_key",
137
+ "header": "Epic"
138
+ },
139
+ {
140
+ "field": "question",
141
+ "header": "Findings"
142
+ },
143
+ {
144
+ "field": "subject_url",
145
+ "header": "Issue"
146
+ },
147
+ {
148
+ "field": "process_key",
149
+ "header": "Process",
150
+ "link": {
151
+ "kind": "processExplorer",
152
+ "keyField": "process_key"
125
153
  }
154
+ },
155
+ {
156
+ "field": "updated_at",
157
+ "header": "Updated"
126
158
  }
127
159
  ],
160
+ "rowKey": "user_task_key",
128
161
  "detail": {
129
162
  "linkField": "subject_url",
130
- "fields": [{ "field": "question", "label": "Review findings" }],
131
- "form": {
132
- "showWhenField": "user_task_key",
133
- "title": "Send the plan back for revision",
134
- "promptField": "question",
135
- "inputKey": "notes",
136
- "inputLabel": "Revision guidance for the planner",
137
- "submitLabel": "Revise \u2014 re-plan",
138
- "action": {
139
- "path": "/app/api/actions/complete-user-task",
140
- "successLabel": "Sent back \u2014 the planner will revise",
141
- "body": {
142
- "userTaskKey": "{{row.user_task_key}}",
143
- "variables": { "directive": "revise", "notes": "{{form.notes}}" }
144
- }
163
+ "fields": [
164
+ {
165
+ "field": "question",
166
+ "label": "Review findings"
145
167
  }
146
- }
147
- }
168
+ ]
169
+ },
170
+ "refreshMs": 5000
148
171
  }
149
172
  },
150
173
  {
@@ -152,58 +175,60 @@
152
175
  "id": "trial-merges",
153
176
  "props": {
154
177
  "title": "Trial-merge decisions",
155
- "collapsible": true,
156
- "defaultCollapsed": false,
157
- "showCount": true,
158
- "rowKey": "user_task_key",
159
- "refreshMs": 5000,
160
178
  "data": {
161
179
  "kind": "datasource",
162
180
  "source": "app",
163
181
  "table": "user_tasks",
164
- "orderBy": { "field": "updated_at", "dir": "desc" },
165
- "filter": [{ "field": "element_id", "in": ["trial-merge-decision"] }]
182
+ "filter": [
183
+ {
184
+ "field": "element_id",
185
+ "in": [
186
+ "trial-merge-decision"
187
+ ]
188
+ }
189
+ ],
190
+ "orderBy": {
191
+ "field": "updated_at",
192
+ "dir": "desc"
193
+ }
166
194
  },
167
195
  "columns": [
168
- { "field": "subject_key", "header": "Epic", "link": { "kind": "page", "page": "epic-detail", "keyField": "subject_key" } },
169
- { "field": "question", "header": "Trial merge" },
170
- { "field": "subject_url", "header": "Issue", "linkField": "subject_url" },
171
- { "field": "process_key", "header": "Process", "link": { "kind": "processExplorer", "keyField": "process_key" } },
172
- { "field": "updated_at", "header": "Updated" }
173
- ],
174
- "rowActions": [
175
196
  {
176
- "label": "Proceed",
177
- "confirm": "Proceed \u2014 accept the red trial merge and continue?",
178
- "showWhenField": "user_task_key",
179
- "action": {
180
- "path": "/app/api/actions/complete-user-task",
181
- "body": { "userTaskKey": "{{row.user_task_key}}", "variables": { "action": "proceed" } }
182
- }
197
+ "field": "subject_key",
198
+ "header": "Epic"
183
199
  },
184
200
  {
185
- "label": "Rebase",
186
- "confirm": "Rebase \u2014 re-run the trial merge?",
187
- "showWhenField": "user_task_key",
188
- "action": {
189
- "path": "/app/api/actions/complete-user-task",
190
- "body": { "userTaskKey": "{{row.user_task_key}}", "variables": { "action": "rebase" } }
191
- }
201
+ "field": "question",
202
+ "header": "Trial merge"
192
203
  },
193
204
  {
194
- "label": "Abandon",
195
- "confirm": "Abandon \u2014 stop and finalize the plan?",
196
- "showWhenField": "user_task_key",
197
- "action": {
198
- "path": "/app/api/actions/complete-user-task",
199
- "body": { "userTaskKey": "{{row.user_task_key}}", "variables": { "action": "abandon" } }
205
+ "field": "subject_url",
206
+ "header": "Issue"
207
+ },
208
+ {
209
+ "field": "process_key",
210
+ "header": "Process",
211
+ "link": {
212
+ "kind": "processExplorer",
213
+ "keyField": "process_key"
200
214
  }
215
+ },
216
+ {
217
+ "field": "updated_at",
218
+ "header": "Updated"
201
219
  }
202
220
  ],
221
+ "rowKey": "user_task_key",
203
222
  "detail": {
204
223
  "linkField": "subject_url",
205
- "fields": [{ "field": "question", "label": "Trial merge" }]
206
- }
224
+ "fields": [
225
+ {
226
+ "field": "question",
227
+ "label": "Trial merge"
228
+ }
229
+ ]
230
+ },
231
+ "refreshMs": 5000
207
232
  }
208
233
  },
209
234
  {
@@ -211,45 +236,56 @@
211
236
  "id": "pr-reviews",
212
237
  "props": {
213
238
  "title": "PR review escalations",
214
- "collapsible": true,
215
- "defaultCollapsed": false,
216
- "showCount": true,
217
- "rowKey": "user_task_key",
218
- "refreshMs": 5000,
219
239
  "data": {
220
240
  "kind": "datasource",
221
241
  "source": "app",
222
242
  "table": "user_tasks",
223
- "orderBy": { "field": "updated_at", "dir": "desc" },
224
- "filter": [{ "field": "element_id", "in": ["wait-answer"] }]
243
+ "filter": [
244
+ {
245
+ "field": "element_id",
246
+ "in": [
247
+ "wait-answer"
248
+ ]
249
+ }
250
+ ],
251
+ "orderBy": {
252
+ "field": "updated_at",
253
+ "dir": "desc"
254
+ }
225
255
  },
226
256
  "columns": [
227
- { "field": "subject_key", "header": "PR", "link": { "kind": "page", "page": "home", "keyField": "subject_key" } },
228
- { "field": "question", "header": "Question" },
229
- { "field": "subject_url", "header": "PR link", "linkField": "subject_url" },
230
- { "field": "process_key", "header": "Process", "link": { "kind": "processExplorer", "keyField": "process_key" } },
231
- { "field": "updated_at", "header": "Updated" }
257
+ {
258
+ "field": "subject_key",
259
+ "header": "PR"
260
+ },
261
+ {
262
+ "field": "subject_url",
263
+ "header": "PR link"
264
+ },
265
+ {
266
+ "field": "process_key",
267
+ "header": "Process",
268
+ "link": {
269
+ "kind": "processExplorer",
270
+ "keyField": "process_key"
271
+ }
272
+ },
273
+ {
274
+ "field": "updated_at",
275
+ "header": "Updated"
276
+ }
232
277
  ],
278
+ "rowKey": "user_task_key",
233
279
  "detail": {
234
280
  "linkField": "subject_url",
235
- "fields": [{ "field": "question", "label": "Escalation question" }],
236
- "form": {
237
- "showWhenField": "user_task_key",
238
- "title": "Answer the review escalation",
239
- "promptField": "question",
240
- "inputKey": "answer",
241
- "inputLabel": "Your answer (resumes the review loop, handed to the next round)",
242
- "submitLabel": "Answer \u2014 resume review",
243
- "action": {
244
- "path": "/app/api/actions/complete-user-task",
245
- "successLabel": "Answered \u2014 the review loop will resume",
246
- "body": {
247
- "userTaskKey": "{{row.user_task_key}}",
248
- "variables": { "answer": "{{form.answer}}" }
249
- }
281
+ "fields": [
282
+ {
283
+ "field": "question",
284
+ "label": "Escalation question"
250
285
  }
251
- }
252
- }
286
+ ]
287
+ },
288
+ "refreshMs": 5000
253
289
  }
254
290
  },
255
291
  {
@@ -257,41 +293,61 @@
257
293
  "id": "blocked-runs",
258
294
  "props": {
259
295
  "title": "Blocked feature runs",
260
- "collapsible": true,
261
- "defaultCollapsed": false,
262
- "showCount": true,
263
- "rowKey": "user_task_key",
264
- "refreshMs": 5000,
265
296
  "data": {
266
297
  "kind": "datasource",
267
298
  "source": "app",
268
299
  "table": "user_tasks",
269
- "orderBy": { "field": "updated_at", "dir": "desc" },
270
- "filter": [{ "field": "element_id", "in": ["feature-blocked"] }]
300
+ "filter": [
301
+ {
302
+ "field": "element_id",
303
+ "in": [
304
+ "feature-blocked"
305
+ ]
306
+ }
307
+ ],
308
+ "orderBy": {
309
+ "field": "updated_at",
310
+ "dir": "desc"
311
+ }
271
312
  },
272
313
  "columns": [
273
- { "field": "subject_key", "header": "Feature", "link": { "kind": "page", "page": "feature", "keyField": "subject_key" } },
274
- { "field": "question", "header": "Disposition" },
275
- { "field": "subject_url", "header": "Issue", "linkField": "subject_url" },
276
- { "field": "process_key", "header": "Process", "link": { "kind": "processExplorer", "keyField": "process_key" } },
277
- { "field": "updated_at", "header": "Updated" }
278
- ],
279
- "rowActions": [
280
314
  {
281
- "label": "Acknowledge blocked",
282
- "confirm": "Acknowledge this blocked run? It settles to terminal blocked (the agent could not open a PR).",
283
- "showWhenField": "user_task_key",
284
- "action": {
285
- "path": "/app/api/actions/acknowledge-blocked",
286
- "body": { "userTaskKey": "{{row.user_task_key}}" }
315
+ "field": "subject_key",
316
+ "header": "Feature"
317
+ },
318
+ {
319
+ "field": "question",
320
+ "header": "Disposition"
321
+ },
322
+ {
323
+ "field": "subject_url",
324
+ "header": "Issue"
325
+ },
326
+ {
327
+ "field": "process_key",
328
+ "header": "Process",
329
+ "link": {
330
+ "kind": "processExplorer",
331
+ "keyField": "process_key"
287
332
  }
333
+ },
334
+ {
335
+ "field": "updated_at",
336
+ "header": "Updated"
288
337
  }
289
338
  ],
339
+ "rowKey": "user_task_key",
290
340
  "detail": {
291
341
  "linkField": "subject_url",
292
- "fields": [{ "field": "question", "label": "Disposition" }]
293
- }
342
+ "fields": [
343
+ {
344
+ "field": "question",
345
+ "label": "Disposition"
346
+ }
347
+ ]
348
+ },
349
+ "refreshMs": 5000
294
350
  }
295
351
  }
296
352
  ]
297
- }
353
+ }
@@ -0,0 +1,58 @@
1
+ // Coverage for the config-key SCAN in the contract gate (scripts/check-contracts.ts, PR #229).
2
+ //
3
+ // The gate is only as strong as the read patterns it recognises. Before this fix it matched only
4
+ // dot-access (`process.env.KEY`), so a config-family key smuggled in through the `envVar("KEY")`
5
+ // helper or string-literal bracket-access silently bypassed the "must be declared" invariant — which
6
+ // is how NANO_PR_WEBHOOK_SECRET / NANO_AGENTIC* stayed undeclared while `check:contracts` passed
7
+ // green. These assert `envKeyReads` sees all three patterns so the gate can hold them to the registry.
8
+ import { test } from "node:test";
9
+ import { assert, assertEquals } from "#test-assert";
10
+ import { envKeyReads, EXPLICIT_CONFIG_KEYS, toPosixRel } from "./check-contracts.ts";
11
+ import { ENV_CONTRACTS } from "../app/contracts.ts";
12
+
13
+ test("envKeyReads: dot-access is recognised", () => {
14
+ assertEquals(envKeyReads("const x = process.env.NANO_PR_POLL_MS;"), ["NANO_PR_POLL_MS"]);
15
+ });
16
+
17
+ test("envKeyReads: string-literal bracket-access is recognised (double and single quote)", () => {
18
+ assert(envKeyReads('process.env["NANO_PR_WEBHOOK_SECRET"]').includes("NANO_PR_WEBHOOK_SECRET"));
19
+ assert(envKeyReads("process.env['NANO_AGENTIC']").includes("NANO_AGENTIC"));
20
+ });
21
+
22
+ test("envKeyReads: the envVar(\"KEY\") helper is recognised", () => {
23
+ assert(envKeyReads('const s = envVar("NANO_AGENTIC_SECRET") ?? "";').includes("NANO_AGENTIC_SECRET"));
24
+ assert(envKeyReads('envVar( "NANO_WORKFORCE_GIT_SHA" )').includes("NANO_WORKFORCE_GIT_SHA"));
25
+ });
26
+
27
+ test("envKeyReads: catches keys across all patterns in one source", () => {
28
+ const src = [
29
+ "const a = process.env.CAMUNDA_TRANSPORT;",
30
+ 'const b = process.env["NANOBPMN_BASE_URL"];',
31
+ 'const c = envVar("NANO_WORKFORCE_GIT_SHA");',
32
+ ].join("\n");
33
+ const keys = new Set(envKeyReads(src));
34
+ assert(keys.has("CAMUNDA_TRANSPORT"));
35
+ assert(keys.has("NANOBPMN_BASE_URL"));
36
+ assert(keys.has("NANO_WORKFORCE_GIT_SHA"));
37
+ });
38
+
39
+ test("envKeyReads: a dynamic (non-literal) read is NOT matched", () => {
40
+ // `process.env[name]` (a variable) can't be resolved statically, so it isn't reported.
41
+ assertEquals(envKeyReads("const v = process.env[name];"), []);
42
+ });
43
+
44
+ test("GITHUB_TOKEN is enforced by the gate and declared in the registry (PR #229 suppressed advisory)", () => {
45
+ // GITHUB_TOKEN is read in production outside the config families (app/service.ts,
46
+ // operations/startPlanFanout.ts). Pin it into the enforced set so the gate holds it to its
47
+ // registry declaration — a future removal from ENV_CONTRACTS while code still reads it must fail.
48
+ assert(EXPLICIT_CONFIG_KEYS.has("GITHUB_TOKEN"), "GITHUB_TOKEN must be an enforced config key");
49
+ assert("GITHUB_TOKEN" in ENV_CONTRACTS, "GITHUB_TOKEN must be declared in ENV_CONTRACTS");
50
+ });
51
+
52
+ test("toPosixRel: normalises Windows backslashes so EXEMPT_FILES matches cross-platform (PR #229 suppressed advisory)", () => {
53
+ // On Windows `join` yields backslashes, so `scripts\check-contracts.ts` would never match the
54
+ // POSIX-style EXEMPT_FILES entry and the checker would self-scan and explode. Normalise them.
55
+ assertEquals(toPosixRel("C:\\repo\\scripts\\check-contracts.ts", "C:\\repo"), "scripts/check-contracts.ts");
56
+ // POSIX paths are already correct and pass through unchanged.
57
+ assertEquals(toPosixRel("/repo/scripts/check-contracts.ts", "/repo"), "scripts/check-contracts.ts");
58
+ });