@nanobpm/nano-workforce 0.37.0 → 0.39.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.
Files changed (37) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +5 -6
  3. package/SPEC.md +15 -13
  4. package/app/abandon.test.ts +4 -4
  5. package/app/abandon.ts +2 -2
  6. package/app/blackboard.test.ts +3 -3
  7. package/app/blackboard.ts +2 -2
  8. package/app/persist-escalation.test.ts +1 -1
  9. package/app/persist-round.test.ts +1 -1
  10. package/app/service.ts +1 -1
  11. package/biome.json +0 -2
  12. package/main.ts +3 -3
  13. package/nano.app.json +12 -25
  14. package/openapi.yaml +653 -0
  15. package/operations/answerFeatureEscalation.test.ts +100 -0
  16. package/operations/answerFeatureEscalation.ts +40 -0
  17. package/operations/appendBlackboard.ts +58 -0
  18. package/{actions → operations}/blackboard.test.ts +16 -10
  19. package/{actions/abandon.test.ts → operations/checkAbandon.test.ts} +5 -11
  20. package/{actions/abandon.ts → operations/checkAbandon.ts} +6 -7
  21. package/operations/getVersion.ts +4 -6
  22. package/operations/listActivePrs.ts +5 -9
  23. package/operations/postMessage.ts +4 -12
  24. package/operations/readBlackboard.ts +26 -0
  25. package/operations/startAndMessage.test.ts +5 -3
  26. package/operations/startConvergenceLoop.ts +16 -19
  27. package/operations/startPlanFanout.ts +14 -19
  28. package/package.json +5 -4
  29. package/pages/epic.page.json +1 -1
  30. package/pages/home.page.json +1 -1
  31. package/tsconfig.json +1 -0
  32. package/workers/persist-task-escalation/worker.ts +1 -1
  33. package/actions/blackboard.ts +0 -77
  34. package/actions/feature-answer-hook.ts +0 -45
  35. package/actions/plan-hook.ts +0 -20
  36. package/actions/webhook-submit.ts +0 -22
  37. package/openapi.json +0 -248
package/openapi.yaml ADDED
@@ -0,0 +1,653 @@
1
+ # Nano Workforce control API — OpenAPI (authoring source, ADR 0059).
2
+ #
3
+ # This YAML is the single source of truth for the app HTTP surface. The Urban toolkit
4
+ # derives typed request/response contracts + runtime validators from it (urban gen), and
5
+ # each operationId is implemented by a delegate in operations/<operationId>.ts. Mounted
6
+ # under /app/api (off the framework-reserved /app page-runtime namespace). Swagger UI is
7
+ # served at /app/api-docs. Prefer editing this file over the generated artifacts.
8
+ openapi: 3.0.3
9
+ info:
10
+ title: Nano Workforce control API
11
+ version: 1.0.0
12
+ description: "The externally-facing control surface an operator, an automation harness, or an LLM
13
+ uses to observe and steer PR-convergence and planning runs. Contract-first (ADR 0058/0059): the
14
+ toolkit derives typed request/response contracts + runtime validators from this document and
15
+ each `operationId` is implemented by a delegate module in `operations/`. Mounted under base
16
+ `/app/api`, kept off the framework-reserved `/app` page-runtime namespace (which owns
17
+ `/app/runtime.js`, `/app/pages/*`, `/app/data/*`). The webhook endpoints are ordinary operations
18
+ too (ADR 0059 — the `actions[]` array is retired): their `/hooks/*` paths in this document are
19
+ relative to the `/app/api` base, so they are served at `/app/api/hooks/*`. Shared-secret and
20
+ capability-token side-channels enforce their credential in the delegate."
21
+ components:
22
+ securitySchemes:
23
+ hookSecret:
24
+ type: apiKey
25
+ in: header
26
+ name: x-hook-secret
27
+ description: Optional shared secret. Enforced by the delegate (NOT the runtime) only when
28
+ NANO_PR_WEBHOOK_SECRET is set; unset means the endpoint is open. Declared here for
29
+ documentation.
30
+ schemas:
31
+ ErrorBody:
32
+ type: object
33
+ required:
34
+ - error
35
+ properties:
36
+ error:
37
+ type: string
38
+ ActivePr:
39
+ type: object
40
+ description: A tracked PR that is not in a terminal (converged/abandoned) state.
41
+ additionalProperties: false
42
+ required:
43
+ - prKey
44
+ - repo
45
+ - number
46
+ - url
47
+ - title
48
+ - status
49
+ - round
50
+ - processKey
51
+ - waitingSince
52
+ - openEscalation
53
+ - updatedAt
54
+ - activeWorker
55
+ - leaseUntil
56
+ properties:
57
+ prKey:
58
+ type: string
59
+ repo:
60
+ type: string
61
+ number:
62
+ type: integer
63
+ url:
64
+ type: string
65
+ title:
66
+ type:
67
+ - string
68
+ - "null"
69
+ status:
70
+ type: string
71
+ round:
72
+ type: integer
73
+ processKey:
74
+ type:
75
+ - string
76
+ - "null"
77
+ description: The engine process instance key; also the keyField the pages processExplorer link uses.
78
+ waitingSince:
79
+ type:
80
+ - string
81
+ - "null"
82
+ openEscalation:
83
+ type:
84
+ - string
85
+ - "null"
86
+ updatedAt:
87
+ type: string
88
+ activeWorker:
89
+ type:
90
+ - string
91
+ - "null"
92
+ leaseUntil:
93
+ type:
94
+ - string
95
+ - "null"
96
+ ActivePrList:
97
+ type: object
98
+ required:
99
+ - count
100
+ - prs
101
+ properties:
102
+ count:
103
+ type: integer
104
+ prs:
105
+ type: array
106
+ items:
107
+ $ref: "#/components/schemas/ActivePr"
108
+ VersionInfo:
109
+ type: object
110
+ description: The running app's identity (which code is actually live).
111
+ additionalProperties: false
112
+ required:
113
+ - name
114
+ - version
115
+ - urbanVersion
116
+ - gitSha
117
+ - gitBranch
118
+ - runtime
119
+ - pid
120
+ - startedAt
121
+ - uptimeSeconds
122
+ properties:
123
+ name:
124
+ type: string
125
+ version:
126
+ type:
127
+ - string
128
+ - "null"
129
+ urbanVersion:
130
+ type:
131
+ - string
132
+ - "null"
133
+ gitSha:
134
+ type:
135
+ - string
136
+ - "null"
137
+ gitBranch:
138
+ type:
139
+ - string
140
+ - "null"
141
+ runtime:
142
+ type: string
143
+ pid:
144
+ type:
145
+ - integer
146
+ - "null"
147
+ startedAt:
148
+ type: string
149
+ uptimeSeconds:
150
+ type: integer
151
+ SubmitResult:
152
+ type: object
153
+ required:
154
+ - prKey
155
+ properties:
156
+ prKey:
157
+ type: string
158
+ processKey:
159
+ type:
160
+ - string
161
+ - "null"
162
+ description: The started convergence-loop instance key (null if the engine did not return one).
163
+ alreadyRunning:
164
+ type: boolean
165
+ description: True when a non-terminal convergence loop for this PR already exists; the aggregate was
166
+ refreshed and no new instance was started.
167
+ StartPlanResult:
168
+ type: object
169
+ required:
170
+ - planKey
171
+ properties:
172
+ planKey:
173
+ type: string
174
+ processKey:
175
+ type:
176
+ - string
177
+ - "null"
178
+ alreadyRunning:
179
+ type: boolean
180
+ description: True when a non-terminal plan for this issue already exists; no new instance was started.
181
+ StartVariables:
182
+ type: object
183
+ description: The start request body. `pr`/`url` (convergence) or `issue`/`url` (planning) name the
184
+ target. Only the fields below are read by the delegate; any extra keys are accepted but
185
+ ignored (they are NOT forwarded to the engine as process variables).
186
+ additionalProperties: true
187
+ properties:
188
+ pr:
189
+ type: string
190
+ description: "PR reference: owner/repo#123 or a PR URL."
191
+ issue:
192
+ type: string
193
+ description: "Issue reference: owner/repo#123 or an issue URL."
194
+ url:
195
+ type: string
196
+ description: Alias for pr/issue when a bare URL is supplied.
197
+ dependsOn:
198
+ type: array
199
+ items:
200
+ type: string
201
+ maxRounds:
202
+ type: integer
203
+ minimum: 1
204
+ description: Values above 100 are accepted and clamped to 100 by the delegate.
205
+ MessageResult:
206
+ type: object
207
+ description: The result of publishing a message / answering an escalation. Shape varies by message
208
+ name; `ok` is always present.
209
+ additionalProperties: true
210
+ required:
211
+ - ok
212
+ properties:
213
+ ok:
214
+ type: boolean
215
+ FeatureAnswerRequest:
216
+ type: object
217
+ description: "Answer an implementation-phase task escalation (issue #25). Supply either `corrKey`,
218
+ or both `plan` and `task`; `answer` is always required."
219
+ required:
220
+ - answer
221
+ properties:
222
+ corrKey:
223
+ type: string
224
+ description: The task correlation key, `<plan_key>:<task_id>` (e.g. owner/repo#12:task-3).
225
+ plan:
226
+ type: string
227
+ description: Plan reference (owner/repo#N); combined with `task` to derive the corrKey.
228
+ task:
229
+ type: string
230
+ description: Task id within the plan; combined with `plan` to derive the corrKey.
231
+ answer:
232
+ type: string
233
+ minLength: 1
234
+ description: The operator's answer that resumes the parked implementation agent.
235
+ BlackboardEntry:
236
+ type: object
237
+ additionalProperties: false
238
+ required:
239
+ - id
240
+ - author_task
241
+ - kind
242
+ - files
243
+ - body
244
+ - wave
245
+ - created_at
246
+ properties:
247
+ id:
248
+ type: integer
249
+ author_task:
250
+ type: string
251
+ kind:
252
+ type: string
253
+ files:
254
+ type: array
255
+ items:
256
+ type: string
257
+ body:
258
+ type: string
259
+ wave:
260
+ type:
261
+ - integer
262
+ - "null"
263
+ created_at:
264
+ type: string
265
+ BlackboardPage:
266
+ type: object
267
+ required:
268
+ - planKey
269
+ - entries
270
+ - cursor
271
+ properties:
272
+ planKey:
273
+ type: string
274
+ entries:
275
+ type: array
276
+ items:
277
+ $ref: "#/components/schemas/BlackboardEntry"
278
+ cursor:
279
+ type:
280
+ - integer
281
+ - "null"
282
+ description: The plan's current head id; pass it back as `since` on the next poll (Tier 2).
283
+ BlackboardAppendRequest:
284
+ type: object
285
+ description: Append one blackboard entry. Idempotent on (plan, dedupe_key).
286
+ required:
287
+ - body
288
+ properties:
289
+ author_task:
290
+ type: string
291
+ description: The authoring task id; defaults to "system" when omitted/blank.
292
+ kind:
293
+ type: string
294
+ description: Entry kind (e.g. note, file-claim); normalized by the delegate.
295
+ files:
296
+ type: array
297
+ items:
298
+ type: string
299
+ description: Files this entry concerns (used by advisory file-claim conflict detection).
300
+ body:
301
+ type: string
302
+ minLength: 1
303
+ description: The note text (required, non-blank).
304
+ wave:
305
+ type: integer
306
+ description: Optional plan wave the entry belongs to.
307
+ dedupe_key:
308
+ type: string
309
+ description: "Idempotency key: a retry with the same (plan, dedupe_key) returns the existing entry."
310
+ BlackboardAppendResult:
311
+ type: object
312
+ required:
313
+ - id
314
+ - inserted
315
+ - conflicts
316
+ properties:
317
+ id:
318
+ type: integer
319
+ inserted:
320
+ type: boolean
321
+ description: False when a prior entry with the same dedupe_key already existed (idempotent retry).
322
+ conflicts:
323
+ type: array
324
+ description: Prior sibling file-claims on the same file(s) (advisory first-writer-wins; never a lock).
325
+ items:
326
+ type: object
327
+ additionalProperties: false
328
+ required:
329
+ - file
330
+ - author_task
331
+ - id
332
+ - body
333
+ - created_at
334
+ properties:
335
+ file:
336
+ type: string
337
+ author_task:
338
+ type: string
339
+ id:
340
+ type: integer
341
+ body:
342
+ type: string
343
+ created_at:
344
+ type: string
345
+ AbandonStatus:
346
+ type: object
347
+ required:
348
+ - prKey
349
+ - status
350
+ - abandoned
351
+ properties:
352
+ prKey:
353
+ type: string
354
+ status:
355
+ type: string
356
+ abandoned:
357
+ type: boolean
358
+ description: Derived from pull_requests.status; true ⇒ the run was cancelled and the agent must stop.
359
+ paths:
360
+ /status:
361
+ get:
362
+ operationId: listActivePrs
363
+ summary: List every tracked PR currently in flight (not converged/abandoned), newest-updated first.
364
+ security:
365
+ - hookSecret: []
366
+ - {}
367
+ responses:
368
+ "200":
369
+ description: The active PRs.
370
+ content:
371
+ application/json:
372
+ schema:
373
+ $ref: "#/components/schemas/ActivePrList"
374
+ "401":
375
+ description: Missing/invalid shared secret (only when NANO_PR_WEBHOOK_SECRET is set).
376
+ content:
377
+ application/json:
378
+ schema:
379
+ $ref: "#/components/schemas/ErrorBody"
380
+ /version:
381
+ get:
382
+ operationId: getVersion
383
+ summary: The running app's identity (app/urban versions, git sha/branch, runtime, pid, uptime).
384
+ security:
385
+ - hookSecret: []
386
+ - {}
387
+ responses:
388
+ "200":
389
+ description: The version/identity payload.
390
+ content:
391
+ application/json:
392
+ schema:
393
+ $ref: "#/components/schemas/VersionInfo"
394
+ "401":
395
+ description: Missing/invalid shared secret (only when NANO_PR_WEBHOOK_SECRET is set).
396
+ content:
397
+ application/json:
398
+ schema:
399
+ $ref: "#/components/schemas/ErrorBody"
400
+ /actions/start/convergence-loop:
401
+ post:
402
+ operationId: startConvergenceLoop
403
+ summary: Register/refresh a PR aggregate (idempotent on prKey) and start its convergence loop.
404
+ requestBody:
405
+ required: true
406
+ content:
407
+ application/json:
408
+ schema:
409
+ $ref: "#/components/schemas/StartVariables"
410
+ responses:
411
+ "202":
412
+ description: The loop was started (or refreshed).
413
+ content:
414
+ application/json:
415
+ schema:
416
+ $ref: "#/components/schemas/SubmitResult"
417
+ "400":
418
+ description: The PR reference could not be parsed.
419
+ content:
420
+ application/json:
421
+ schema:
422
+ $ref: "#/components/schemas/ErrorBody"
423
+ /actions/start/plan-fanout:
424
+ post:
425
+ operationId: startPlanFanout
426
+ summary: Register/refresh a plan aggregate (idempotent on planKey) and start the planning fan-out.
427
+ requestBody:
428
+ required: true
429
+ content:
430
+ application/json:
431
+ schema:
432
+ $ref: "#/components/schemas/StartVariables"
433
+ responses:
434
+ "202":
435
+ description: The plan fan-out was started (or was already running).
436
+ content:
437
+ application/json:
438
+ schema:
439
+ $ref: "#/components/schemas/StartPlanResult"
440
+ "400":
441
+ description: The issue reference could not be parsed.
442
+ content:
443
+ application/json:
444
+ schema:
445
+ $ref: "#/components/schemas/ErrorBody"
446
+ /actions/message:
447
+ post:
448
+ operationId: postMessage
449
+ summary: Publish a message / answer an escalation. For escalation-answered and
450
+ feature-escalation-answered names, runs the corresponding answer flow; otherwise a plain
451
+ publishMessage.
452
+ requestBody:
453
+ required: true
454
+ content:
455
+ application/json:
456
+ schema:
457
+ type: object
458
+ additionalProperties: true
459
+ required:
460
+ - name
461
+ properties:
462
+ name:
463
+ type: string
464
+ minLength: 1
465
+ description: The message name (correlates a waiting event).
466
+ correlationKey:
467
+ type: string
468
+ variables:
469
+ type: object
470
+ additionalProperties: true
471
+ properties:
472
+ answer:
473
+ type: string
474
+ responses:
475
+ "200":
476
+ description: The message was published (or the escalation answered).
477
+ content:
478
+ application/json:
479
+ schema:
480
+ $ref: "#/components/schemas/MessageResult"
481
+ "400":
482
+ description: A required field was missing/blank.
483
+ content:
484
+ application/json:
485
+ schema:
486
+ $ref: "#/components/schemas/ErrorBody"
487
+ "404":
488
+ description: No matching open escalation / parked token to answer.
489
+ content:
490
+ application/json:
491
+ schema:
492
+ $ref: "#/components/schemas/MessageResult"
493
+ /hooks/feature-answer:
494
+ post:
495
+ operationId: answerFeatureEscalation
496
+ summary: "Answer an implementation-phase task escalation out of band (issue #25). Optional
497
+ shared-secret guard (x-hook-secret), enforced only when NANO_PR_WEBHOOK_SECRET is set."
498
+ security:
499
+ - hookSecret: []
500
+ - {}
501
+ requestBody:
502
+ required: true
503
+ content:
504
+ application/json:
505
+ schema:
506
+ $ref: "#/components/schemas/FeatureAnswerRequest"
507
+ responses:
508
+ "200":
509
+ description: The escalation was answered and the parked agent resumed.
510
+ content:
511
+ application/json:
512
+ schema:
513
+ $ref: "#/components/schemas/MessageResult"
514
+ "400":
515
+ description: A required field was missing (answer, or a resolvable correlation key).
516
+ content:
517
+ application/json:
518
+ schema:
519
+ $ref: "#/components/schemas/MessageResult"
520
+ "401":
521
+ description: Missing/invalid shared secret (only when NANO_PR_WEBHOOK_SECRET is set).
522
+ content:
523
+ application/json:
524
+ schema:
525
+ $ref: "#/components/schemas/MessageResult"
526
+ "404":
527
+ description: No matching open task escalation for the correlation key.
528
+ content:
529
+ application/json:
530
+ schema:
531
+ $ref: "#/components/schemas/MessageResult"
532
+ /hooks/blackboard:
533
+ get:
534
+ operationId: readBlackboard
535
+ summary: Read a plan's blackboard, scoped by the capability token (Tier 1). Optional ?since=<id>
536
+ returns only newer entries.
537
+ parameters:
538
+ - name: token
539
+ in: query
540
+ required: false
541
+ schema:
542
+ type: string
543
+ description: "Per-plan capability token (issues #51 / #49) — the token IS the credential and
544
+ scopes the read to exactly one plan. Required in practice, but declared optional here
545
+ because it may instead be sent as the `x-blackboard-token` header; the delegate enforces
546
+ it (400 when absent from both, 404 when unknown). Not an OpenAPI `security` scheme: it is
547
+ a per-plan DB-issued capability, not a fixed shared secret the runtime could verify."
548
+ - name: since
549
+ in: query
550
+ required: false
551
+ schema:
552
+ type: integer
553
+ description: Return only entries with id > since (incremental poll).
554
+ responses:
555
+ "200":
556
+ description: The blackboard page for this plan.
557
+ content:
558
+ application/json:
559
+ schema:
560
+ $ref: "#/components/schemas/BlackboardPage"
561
+ "400":
562
+ description: Missing blackboard token.
563
+ content:
564
+ application/json:
565
+ schema:
566
+ $ref: "#/components/schemas/ErrorBody"
567
+ "404":
568
+ description: Unknown blackboard token (never reveals which plans exist).
569
+ content:
570
+ application/json:
571
+ schema:
572
+ $ref: "#/components/schemas/ErrorBody"
573
+ post:
574
+ operationId: appendBlackboard
575
+ summary: Append one entry to a plan's blackboard, scoped by the capability token (Tier 1).
576
+ Idempotent on (plan, dedupe_key).
577
+ parameters:
578
+ - name: token
579
+ in: query
580
+ required: false
581
+ schema:
582
+ type: string
583
+ description: "Per-plan capability token (issues #51 / #49) — the token IS the credential and
584
+ scopes the write to exactly one plan. Required in practice, but declared optional here
585
+ because it may instead be sent as the `x-blackboard-token` header; the delegate enforces
586
+ it (400 when absent from both, 404 when unknown). Not an OpenAPI `security` scheme: it is
587
+ a per-plan DB-issued capability, not a fixed shared secret the runtime could verify."
588
+ requestBody:
589
+ required: true
590
+ content:
591
+ application/json:
592
+ schema:
593
+ $ref: "#/components/schemas/BlackboardAppendRequest"
594
+ responses:
595
+ "200":
596
+ description: "An idempotent retry: the entry already existed (inserted=false)."
597
+ content:
598
+ application/json:
599
+ schema:
600
+ $ref: "#/components/schemas/BlackboardAppendResult"
601
+ "201":
602
+ description: The entry was appended.
603
+ content:
604
+ application/json:
605
+ schema:
606
+ $ref: "#/components/schemas/BlackboardAppendResult"
607
+ "400":
608
+ description: Missing blackboard token or blank body.
609
+ content:
610
+ application/json:
611
+ schema:
612
+ $ref: "#/components/schemas/ErrorBody"
613
+ "404":
614
+ description: Unknown blackboard token (never reveals which plans exist).
615
+ content:
616
+ application/json:
617
+ schema:
618
+ $ref: "#/components/schemas/ErrorBody"
619
+ /hooks/abandon:
620
+ get:
621
+ operationId: checkAbandon
622
+ summary: "Cooperative abandon check (issue #76): tell a running agent whether its run was cancelled.
623
+ Scoped by the per-PR capability token."
624
+ parameters:
625
+ - name: token
626
+ in: query
627
+ required: false
628
+ schema:
629
+ type: string
630
+ description: "Per-PR capability token (issue #76) — the token IS the credential and scopes the
631
+ read to exactly one PR. Required in practice, but declared optional here because it may
632
+ instead be sent as the `x-abandon-token` header; the delegate enforces it (400 when absent
633
+ from both, 404 when unknown). Not an OpenAPI `security` scheme: it is a per-PR DB-issued
634
+ capability, not a fixed shared secret the runtime could verify."
635
+ responses:
636
+ "200":
637
+ description: The PR's abandon status.
638
+ content:
639
+ application/json:
640
+ schema:
641
+ $ref: "#/components/schemas/AbandonStatus"
642
+ "400":
643
+ description: Missing abandon token.
644
+ content:
645
+ application/json:
646
+ schema:
647
+ $ref: "#/components/schemas/ErrorBody"
648
+ "404":
649
+ description: Unknown abandon token (never reveals which PRs exist).
650
+ content:
651
+ application/json:
652
+ schema:
653
+ $ref: "#/components/schemas/ErrorBody"