@nanobpm/nano-workforce 0.38.0 → 0.39.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/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ ## [0.39.1](https://github.com/nanobpm/nano-workforce/compare/v0.39.0...v0.39.1) (2026-08-10)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * **openapi:** use 3.0 nullable idiom instead of 3.1 type-arrays ([#111](https://github.com/nanobpm/nano-workforce/issues/111)) ([28f40c8](https://github.com/nanobpm/nano-workforce/commit/28f40c891d48043ef7537158466bafdfb79b9792))
7
+
8
+ # [0.39.0](https://github.com/nanobpm/nano-workforce/compare/v0.38.0...v0.39.0) (2026-08-10)
9
+
10
+
11
+ ### Features
12
+
13
+ * adopt urban 0.38.0 typed delegate registry for the OpenAPI surface ([#110](https://github.com/nanobpm/nano-workforce/issues/110)) ([fe62908](https://github.com/nanobpm/nano-workforce/commit/fe62908968c0bd707f8e2da58fa550cca9fbab5b))
14
+
1
15
  # [0.38.0](https://github.com/nanobpm/nano-workforce/compare/v0.37.0...v0.38.0) (2026-08-10)
2
16
 
3
17
 
package/nano.app.json CHANGED
@@ -124,6 +124,16 @@
124
124
  "handler": "workers/retro-record/worker.ts"
125
125
  }
126
126
  ],
127
+ "externalTaskTypes": [
128
+ "senior:pr-review",
129
+ "senior:fix-ci",
130
+ "senior:rebase",
131
+ "senior:plan",
132
+ "senior:plan-review",
133
+ "senior:feature",
134
+ "senior:trial-merge",
135
+ "senior:retro"
136
+ ],
127
137
  "surfaces": {
128
138
  "pages": {
129
139
  "enabled": true,
package/openapi.yaml CHANGED
@@ -38,6 +38,7 @@ components:
38
38
  ActivePr:
39
39
  type: object
40
40
  description: A tracked PR that is not in a terminal (converged/abandoned) state.
41
+ additionalProperties: false
41
42
  required:
42
43
  - prKey
43
44
  - repo
@@ -62,36 +63,30 @@ components:
62
63
  url:
63
64
  type: string
64
65
  title:
65
- type:
66
- - string
67
- - "null"
66
+ type: string
67
+ nullable: true
68
68
  status:
69
69
  type: string
70
70
  round:
71
71
  type: integer
72
72
  processKey:
73
- type:
74
- - string
75
- - "null"
73
+ type: string
74
+ nullable: true
76
75
  description: The engine process instance key; also the keyField the pages processExplorer link uses.
77
76
  waitingSince:
78
- type:
79
- - string
80
- - "null"
77
+ type: string
78
+ nullable: true
81
79
  openEscalation:
82
- type:
83
- - string
84
- - "null"
80
+ type: string
81
+ nullable: true
85
82
  updatedAt:
86
83
  type: string
87
84
  activeWorker:
88
- type:
89
- - string
90
- - "null"
85
+ type: string
86
+ nullable: true
91
87
  leaseUntil:
92
- type:
93
- - string
94
- - "null"
88
+ type: string
89
+ nullable: true
95
90
  ActivePrList:
96
91
  type: object
97
92
  required:
@@ -107,6 +102,7 @@ components:
107
102
  VersionInfo:
108
103
  type: object
109
104
  description: The running app's identity (which code is actually live).
105
+ additionalProperties: false
110
106
  required:
111
107
  - name
112
108
  - version
@@ -121,27 +117,22 @@ components:
121
117
  name:
122
118
  type: string
123
119
  version:
124
- type:
125
- - string
126
- - "null"
120
+ type: string
121
+ nullable: true
127
122
  urbanVersion:
128
- type:
129
- - string
130
- - "null"
123
+ type: string
124
+ nullable: true
131
125
  gitSha:
132
- type:
133
- - string
134
- - "null"
126
+ type: string
127
+ nullable: true
135
128
  gitBranch:
136
- type:
137
- - string
138
- - "null"
129
+ type: string
130
+ nullable: true
139
131
  runtime:
140
132
  type: string
141
133
  pid:
142
- type:
143
- - integer
144
- - "null"
134
+ type: integer
135
+ nullable: true
145
136
  startedAt:
146
137
  type: string
147
138
  uptimeSeconds:
@@ -154,9 +145,8 @@ components:
154
145
  prKey:
155
146
  type: string
156
147
  processKey:
157
- type:
158
- - string
159
- - "null"
148
+ type: string
149
+ nullable: true
160
150
  description: The started convergence-loop instance key (null if the engine did not return one).
161
151
  alreadyRunning:
162
152
  type: boolean
@@ -170,9 +160,8 @@ components:
170
160
  planKey:
171
161
  type: string
172
162
  processKey:
173
- type:
174
- - string
175
- - "null"
163
+ type: string
164
+ nullable: true
176
165
  alreadyRunning:
177
166
  type: boolean
178
167
  description: True when a non-terminal plan for this issue already exists; no new instance was started.
@@ -232,6 +221,7 @@ components:
232
221
  description: The operator's answer that resumes the parked implementation agent.
233
222
  BlackboardEntry:
234
223
  type: object
224
+ additionalProperties: false
235
225
  required:
236
226
  - id
237
227
  - author_task
@@ -254,9 +244,8 @@ components:
254
244
  body:
255
245
  type: string
256
246
  wave:
257
- type:
258
- - integer
259
- - "null"
247
+ type: integer
248
+ nullable: true
260
249
  created_at:
261
250
  type: string
262
251
  BlackboardPage:
@@ -273,9 +262,8 @@ components:
273
262
  items:
274
263
  $ref: "#/components/schemas/BlackboardEntry"
275
264
  cursor:
276
- type:
277
- - integer
278
- - "null"
265
+ type: integer
266
+ nullable: true
279
267
  description: The plan's current head id; pass it back as `since` on the next poll (Tier 2).
280
268
  BlackboardAppendRequest:
281
269
  type: object
@@ -321,6 +309,7 @@ components:
321
309
  description: Prior sibling file-claims on the same file(s) (advisory first-writer-wins; never a lock).
322
310
  items:
323
311
  type: object
312
+ additionalProperties: false
324
313
  required:
325
314
  - file
326
315
  - author_task
@@ -10,25 +10,16 @@
10
10
  // Body accepts either the raw correlation key or a plan+task pair:
11
11
  // { "corrKey": "owner/repo#12:task-3", "answer": "…" }
12
12
  // { "plan": "owner/repo#12", "task": "task-3", "answer": "…" }
13
- import { defineOperation } from "@nanobpm/urban";
13
+
14
14
  import { answerTaskEscalation, featureCorrKey } from "../app/plan.ts";
15
15
  import { envVar } from "../app/version.ts";
16
+ import { defineOperation } from "../nano-generated/operations.ts";
16
17
 
17
18
  const WEBHOOK_SECRET = envVar("NANO_PR_WEBHOOK_SECRET") ?? "";
18
19
 
19
20
  const str = (v: unknown): string => (typeof v === "string" ? v.trim() : "");
20
21
 
21
- interface Body {
22
- corrKey?: unknown;
23
- plan?: unknown;
24
- task?: unknown;
25
- answer?: unknown;
26
- }
27
-
28
- export default defineOperation<
29
- { params: Record<string, string>; query: Record<string, string | string[] | undefined>; body: Body },
30
- { ok: boolean } & Record<string, unknown>
31
- >("answerFeatureEscalation", async ({ req, body }, app) => {
22
+ export default defineOperation("answerFeatureEscalation", async ({ req, body }, app) => {
32
23
  if (WEBHOOK_SECRET && req.headers.get("x-hook-secret") !== WEBHOOK_SECRET) {
33
24
  return { status: 401, body: { ok: false, error: "unauthorized" } };
34
25
  }
@@ -7,19 +7,16 @@
7
7
  // POST → append one entry: { author_task?, kind?, files?, body, wave?, dedupe_key? }. Idempotent
8
8
  // on (plan, dedupe_key). Returns { id, inserted, conflicts } — `conflicts` lists prior
9
9
  // sibling `file-claim`s on the same file(s) (advisory first-writer-wins; never a lock).
10
- import { defineOperation } from "@nanobpm/urban";
11
- import type { ClaimConflict } from "../app/blackboard.ts";
10
+
12
11
  import {
13
12
  appendEntry,
14
13
  detectFileClaimConflicts,
15
14
  normalizeKind,
16
15
  planKeyForToken,
17
16
  } from "../app/blackboard.ts";
17
+ import { defineOperation } from "../nano-generated/operations.ts";
18
18
 
19
- export default defineOperation<
20
- { params: Record<string, string>; query: { token?: string }; body: Record<string, unknown> },
21
- { id: number; inserted: boolean; conflicts: ClaimConflict[] } | { error: string }
22
- >("appendBlackboard", async ({ req, body }, app) => {
19
+ export default defineOperation("appendBlackboard", async ({ req, body }, app) => {
23
20
  const token = (req.query.get("token") ?? req.headers.get("x-blackboard-token") ?? "").trim();
24
21
  if (!token) return { status: 400, body: { error: "missing blackboard token" } };
25
22
  const planKey = await planKeyForToken(app.data, token);
@@ -9,13 +9,11 @@
9
9
  //
10
10
  // GET → { prKey, status, abandoned } — `abandoned` is derived from `pull_requests.status`,
11
11
  // which Urban's cancel primitive sets to 'abandoned' on cancel. `true` ⇒ the agent must stop.
12
- import { defineOperation } from "@nanobpm/urban";
12
+
13
13
  import { abandonStatusForToken } from "../app/abandon.ts";
14
+ import { defineOperation } from "../nano-generated/operations.ts";
14
15
 
15
- export default defineOperation<
16
- { params: Record<string, string>; query: { token?: string }; body: unknown },
17
- { prKey: string; status: string; abandoned: boolean } | { error: string }
18
- >("checkAbandon", async ({ req }, app) => {
16
+ export default defineOperation("checkAbandon", async ({ req }, app) => {
19
17
  const token = (req.query.get("token") ?? req.headers.get("x-abandon-token") ?? "").trim();
20
18
  if (!token) return { status: 400, body: { error: "missing abandon token" } };
21
19
  const state = await abandonStatusForToken(app.data, token);
@@ -6,17 +6,15 @@
6
6
  // Read-only and unauthenticated by design (no secrets in the payload). The optional shared-secret
7
7
  // guard stays HERE (the runtime does not enforce OpenAPI `security`); the OpenAPI document only
8
8
  // routes GET to this operation, so a wrong method is a 404 from the router (no explicit 405 needed).
9
- import { defineOperation } from "@nanobpm/urban";
10
- import { buildVersionInfo, envVar, type VersionInfo } from "../app/version.ts";
9
+
10
+ import { buildVersionInfo, envVar } from "../app/version.ts";
11
+ import { defineOperation } from "../nano-generated/operations.ts";
11
12
 
12
13
  // The optional shared-secret guard: when NANO_PR_WEBHOOK_SECRET is set, callers must present it via
13
14
  // the x-hook-secret header. Captured once, at module load.
14
15
  const SECRET = envVar("NANO_PR_WEBHOOK_SECRET") ?? "";
15
16
 
16
- export default defineOperation<
17
- { params: Record<string, string>; query: Record<string, string | string[] | undefined>; body: undefined },
18
- VersionInfo | { error: string }
19
- >("getVersion", ({ req }) => {
17
+ export default defineOperation("getVersion", ({ req }) => {
20
18
  if (SECRET && req.headers.get("x-hook-secret") !== SECRET) {
21
19
  return { status: 401, body: { error: "unauthorized" } };
22
20
  }
@@ -6,20 +6,16 @@
6
6
  // The runtime validates the (empty) request against openapi.yaml; the optional shared-secret guard
7
7
  // stays HERE (the runtime does not enforce OpenAPI `security`): when NANO_PR_WEBHOOK_SECRET is set,
8
8
  // callers must present it via the x-hook-secret header. Unset → open (unchanged default).
9
- import { defineOperation } from "@nanobpm/urban";
10
- import { type ActivePr, activePrs } from "../app/service.ts";
9
+
10
+ import { activePrs } from "../app/service.ts";
11
11
  import { envVar } from "../app/version.ts";
12
+ import { defineOperation } from "../nano-generated/operations.ts";
12
13
 
13
14
  // The optional shared-secret guard: when NANO_PR_WEBHOOK_SECRET is set, callers must present it via
14
15
  // the x-hook-secret header. Captured once, at module load.
15
16
  const SECRET = envVar("NANO_PR_WEBHOOK_SECRET") ?? "";
16
17
 
17
- type Res = { count: number; prs: ActivePr[] } | { error: string };
18
-
19
- export default defineOperation<
20
- { params: Record<string, string>; query: Record<string, string | string[] | undefined>; body: undefined },
21
- Res
22
- >("listActivePrs", async ({ req }, app) => {
18
+ export default defineOperation("listActivePrs", async ({ req }, app) => {
23
19
  if (SECRET && req.headers.get("x-hook-secret") !== SECRET) {
24
20
  return { status: 401, body: { error: "unauthorized" } };
25
21
  }
@@ -8,20 +8,12 @@
8
8
  // The runtime validates the body against openapi.yaml (`name` is required, so a missing name is a 400
9
9
  // for free); this delegate keeps the message-name dispatch — the discriminator + downstream behavior
10
10
  // is app logic, not something the JSON schema can express.
11
- import { defineOperation } from "@nanobpm/urban";
11
+
12
12
  import { answerTaskEscalation, FEATURE_ESCALATION_MESSAGE } from "../app/plan.ts";
13
13
  import { answerEscalation } from "../app/service.ts";
14
+ import { defineOperation } from "../nano-generated/operations.ts";
14
15
 
15
- interface Body {
16
- name?: unknown;
17
- correlationKey?: unknown;
18
- variables?: Record<string, unknown>;
19
- }
20
-
21
- export default defineOperation<
22
- { params: Record<string, string>; query: Record<string, string | string[] | undefined>; body: Body },
23
- Record<string, unknown>
24
- >("postMessage", async ({ body }, app) => {
16
+ export default defineOperation("postMessage", async ({ body }, app) => {
25
17
  const b = body ?? {};
26
18
  const name = String(b.name ?? "");
27
19
  if (!name) return { status: 400, body: { error: "name is required" } };
@@ -9,14 +9,11 @@
9
9
  // GET → { planKey, entries: [ { id, author_task, kind, files, body, wave, created_at } ], cursor }
10
10
  // optional ?since=<id> returns only entries with id > since (incremental poll). `cursor` is
11
11
  // the plan's current head id; pass it back as `since` on the next poll (Tier 2).
12
- import { defineOperation } from "@nanobpm/urban";
13
- import type { BlackboardPage } from "../app/blackboard.ts";
12
+
14
13
  import { planKeyForToken, readBlackboardPage } from "../app/blackboard.ts";
14
+ import { defineOperation } from "../nano-generated/operations.ts";
15
15
 
16
- export default defineOperation<
17
- { params: Record<string, string>; query: { token?: string; since?: string }; body: unknown },
18
- (BlackboardPage & { planKey: string }) | { error: string }
19
- >("readBlackboard", async ({ req }, app) => {
16
+ export default defineOperation("readBlackboard", async ({ req }, app) => {
20
17
  const token = (req.query.get("token") ?? req.headers.get("x-blackboard-token") ?? "").trim();
21
18
  if (!token) return { status: 400, body: { error: "missing blackboard token" } };
22
19
  const planKey = await planKeyForToken(app.data, token);
@@ -8,20 +8,11 @@
8
8
  // not leak the engine's variable-map concept to callers. The runtime validates the body against
9
9
  // openapi.yaml; this delegate keeps the PR-parse guard because the reference format (owner/repo#123
10
10
  // or a URL) is app logic, not something the JSON schema can express — an unparseable reference is a 400.
11
- import { defineOperation } from "@nanobpm/urban";
12
- import { clampRounds, MAX_ROUNDS, parsePr, submitPr } from "../app/service.ts";
13
11
 
14
- interface Body {
15
- pr?: string;
16
- url?: string;
17
- dependsOn?: unknown;
18
- maxRounds?: unknown;
19
- }
12
+ import { clampRounds, MAX_ROUNDS, parsePr, submitPr } from "../app/service.ts";
13
+ import { defineOperation } from "../nano-generated/operations.ts";
20
14
 
21
- export default defineOperation<
22
- { params: Record<string, string>; query: Record<string, string | string[] | undefined>; body: Body },
23
- { prKey: string; alreadyRunning?: boolean; processKey?: string | null } | { error: string }
24
- >("startConvergenceLoop", async ({ body }, app) => {
15
+ export default defineOperation("startConvergenceLoop", async ({ body }, app) => {
25
16
  const b = body ?? {};
26
17
  const raw = String(b.pr ?? b.url ?? "").trim();
27
18
  const parsed = parsePr(raw);
@@ -7,22 +7,11 @@
7
7
  //
8
8
  // The request body is FLAT (`{ issue | url }`), not wrapped in a `variables` envelope — this is a
9
9
  // purpose-built operation, not a generic engine "start process" call.
10
- import { defineOperation } from "@nanobpm/urban";
11
- import { parseIssue, startPlan } from "../app/plan.ts";
12
-
13
- interface Body {
14
- issue?: string;
15
- url?: string;
16
- }
17
10
 
18
- type Res =
19
- | { planKey: string; alreadyRunning?: boolean; processKey?: string | null }
20
- | { error: string };
11
+ import { parseIssue, startPlan } from "../app/plan.ts";
12
+ import { defineOperation } from "../nano-generated/operations.ts";
21
13
 
22
- export default defineOperation<
23
- { params: Record<string, string>; query: Record<string, string | string[] | undefined>; body: Body },
24
- Res
25
- >("startPlanFanout", async ({ body }, app) => {
14
+ export default defineOperation("startPlanFanout", async ({ body }, app) => {
26
15
  const b = body ?? {};
27
16
  const raw = String(b.issue ?? b.url ?? "").trim();
28
17
  const parsed = parseIssue(raw);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nanobpm/nano-workforce",
3
- "version": "0.38.0",
3
+ "version": "0.39.1",
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",
@@ -33,6 +33,7 @@
33
33
  "upgrade": "node --experimental-strip-types scripts/upgrade-from-pack.ts",
34
34
  "check": "urban check",
35
35
  "typecheck": "tsc --noEmit",
36
+ "pretypecheck": "urban gen",
36
37
  "check:prompts": "node --experimental-strip-types scripts/check-agent-prompts.ts",
37
38
  "gen": "urban gen",
38
39
  "gen:check": "urban gen --check",
@@ -44,7 +45,7 @@
44
45
  "lint:fix": "biome check --write app operations workers pages components scripts main.ts"
45
46
  },
46
47
  "dependencies": {
47
- "@nanobpm/urban": "^0.35.0"
48
+ "@nanobpm/urban": "^0.38.0"
48
49
  },
49
50
  "devDependencies": {
50
51
  "@biomejs/biome": "^2.4.11",
package/tsconfig.json CHANGED
@@ -39,6 +39,7 @@
39
39
  "main.ts",
40
40
  "app/**/*.ts",
41
41
  "operations/**/*.ts",
42
+ "nano-generated/**/*.ts",
42
43
  "workers/**/*.ts",
43
44
  "lib/**/*.ts",
44
45
  "src/**/*.ts",