@nanobpm/nano-workforce 0.202.0 → 0.203.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 (77) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/SPEC.md +48 -0
  3. package/app/admittedStaging.test.ts +71 -0
  4. package/app/agentActivationLedger.ts +5 -0
  5. package/app/agentCompletion.test.ts +33 -0
  6. package/app/agentCompletion.ts +108 -2
  7. package/app/agentic/vocab/agent-sla-boundary.test.ts +6 -1
  8. package/app/contracts.ts +23 -0
  9. package/app/deliveryGraphDispatch.ts +6 -0
  10. package/app/deliveryGraphReadModel.test.ts +2 -2
  11. package/app/deliveryGraphRun.ts +18 -0
  12. package/app/deliveryRunner.ts +9 -1
  13. package/app/escalationSteward.test.ts +452 -0
  14. package/app/escalationSteward.ts +537 -0
  15. package/app/escalationStewardPoll.test.ts +123 -0
  16. package/app/feature.test.ts +16 -0
  17. package/app/feature.ts +11 -0
  18. package/app/featureDelivery.test.ts +23 -0
  19. package/app/mcpToolSurface.ts +17 -1
  20. package/app/plan.ts +49 -6
  21. package/app/planLowering.test.ts +26 -1
  22. package/app/planLowering.ts +5 -0
  23. package/app/promotionPoll.test.ts +19 -0
  24. package/app/pullRequestReadModel.test.ts +1 -1
  25. package/app/service.test.ts +36 -0
  26. package/app/service.ts +139 -5
  27. package/db/migrations/120_escalation_auto_resolutions.sql +229 -0
  28. package/db/migrations/121_delivery_graph_read_model_auto_resolve.sql +54 -0
  29. package/db/migrations/122_pull_requests_read_model_auto_resolve.sql +56 -0
  30. package/db/migrations/123_escalation_auto_resolutions_selected_option.sql +47 -0
  31. package/db/migrations/124_admitted_epics_auto_resolve.sql +21 -0
  32. package/docs/agent-guide.md +69 -30
  33. package/docs/mcp-runbook.md +2 -0
  34. package/e2e/escalation-steward.e2e.ts +211 -0
  35. package/nano.app.json +10 -1
  36. package/openapi.yaml +365 -0
  37. package/operations/dispatchDeliveryGraph.ts +6 -1
  38. package/operations/listActivePrs.test.ts +60 -1
  39. package/operations/listActivePrs.ts +3 -3
  40. package/operations/listAutoResolutions.ts +83 -0
  41. package/operations/reviewAutoResolution.test.ts +137 -0
  42. package/operations/reviewAutoResolution.ts +42 -0
  43. package/operations/startConvergenceLoop.ts +5 -1
  44. package/operations/startEpicSet.admission.integration.test.ts +2 -2
  45. package/operations/startEpicSet.ts +8 -2
  46. package/operations/startFeature.ts +5 -0
  47. package/operations/startPlanFanout.ts +4 -1
  48. package/package.json +1 -1
  49. package/pages/_nav.json +12 -0
  50. package/pages/auto-resolutions.page.json +191 -0
  51. package/pages/board.page.json +17 -0
  52. package/pages/cockpit.page.json +17 -0
  53. package/pages/delivery-graph-detail.page.json +17 -0
  54. package/pages/delivery-graphs/staged.mount.js +17 -5
  55. package/pages/delivery-graphs.page.json +17 -0
  56. package/pages/epic-detail.page.json +17 -0
  57. package/pages/epic.page.json +19 -1
  58. package/pages/feature.page.json +19 -1
  59. package/pages/home.page.json +22 -0
  60. package/pages/lineage.page.json +17 -0
  61. package/pages/mcp.page.json +17 -0
  62. package/pages/overview.page.json +17 -0
  63. package/pages/tasks.page.json +17 -0
  64. package/pages/velocity.page.json +17 -0
  65. package/resources/processes/escalation-steward.bpmn +162 -0
  66. package/resources/prompts/escalation-steward.md +87 -0
  67. package/test/delivery-graphs-staged-embed.test.ts +13 -0
  68. package/test/derivation-parity/derivation-parity.test.ts +1 -0
  69. package/test/derivation-parity/flows.ts +12 -0
  70. package/workers/converge-feature/worker.ts +6 -1
  71. package/workers/delivery-connector/worker.test.ts +72 -1
  72. package/workers/delivery-connector/worker.ts +53 -2
  73. package/workers/escalation-steward-collect/worker.test.ts +180 -0
  74. package/workers/escalation-steward-collect/worker.ts +99 -0
  75. package/workers/escalation-steward-record/worker.test.ts +740 -0
  76. package/workers/escalation-steward-record/worker.ts +428 -0
  77. package/workers/record-wave/worker.ts +5 -1
package/openapi.yaml CHANGED
@@ -91,6 +91,7 @@ components:
91
91
  - updatedAt
92
92
  - activeWorker
93
93
  - leaseUntil
94
+ - autoResolveEscalations
94
95
  properties:
95
96
  prKey:
96
97
  type: string
@@ -148,11 +149,18 @@ components:
148
149
  leaseUntil:
149
150
  type: string
150
151
  nullable: true
152
+ autoResolveEscalations:
153
+ type: boolean
154
+ description: >-
155
+ Whether this PR's run opted into automatic escalation resolution by the escalation
156
+ steward (issue #889). Derived from the subject row's durable `auto_resolve_escalations`
157
+ flag, never a clobbered process variable.
151
158
  ActivePrList:
152
159
  type: object
153
160
  required:
154
161
  - count
155
162
  - prs
163
+ - deliveries
156
164
  properties:
157
165
  count:
158
166
  type: integer
@@ -160,6 +168,68 @@ components:
160
168
  type: array
161
169
  items:
162
170
  $ref: "#/components/schemas/ActivePr"
171
+ deliveries:
172
+ type: array
173
+ description: >-
174
+ Every delivery-graph run currently in flight (not done/failed/abandoned), newest-updated
175
+ first (issue #889 §1 — the opt-in is shown for each PR AND each delivery).
176
+ items:
177
+ $ref: "#/components/schemas/ActiveDelivery"
178
+ ActiveDelivery:
179
+ type: object
180
+ description: A delivery-graph run currently in flight, as reported by the status endpoint.
181
+ additionalProperties: false
182
+ required:
183
+ - runKey
184
+ - title
185
+ - status
186
+ - phase
187
+ - processKey
188
+ - openEscalation
189
+ - updatedAt
190
+ - autoResolveEscalations
191
+ properties:
192
+ runKey:
193
+ type: string
194
+ title:
195
+ type: string
196
+ nullable: true
197
+ status:
198
+ type: string
199
+ phase:
200
+ type: string
201
+ nullable: true
202
+ processKey:
203
+ type: string
204
+ nullable: true
205
+ description: The engine process instance key; null while the launch claim has not been stamped yet.
206
+ openEscalation:
207
+ type: object
208
+ nullable: true
209
+ additionalProperties: false
210
+ description: >-
211
+ The structured pointer to this run's OPEN escalation/human task when parked awaiting an
212
+ answer (derived from the same `user_tasks` read model as `listEscalations`), else null.
213
+ required:
214
+ - userTaskKey
215
+ - kind
216
+ - summary
217
+ properties:
218
+ userTaskKey:
219
+ type: string
220
+ kind:
221
+ type: string
222
+ summary:
223
+ type: string
224
+ nullable: true
225
+ updatedAt:
226
+ type: string
227
+ autoResolveEscalations:
228
+ type: boolean
229
+ description: >-
230
+ Whether this delivery run opted into automatic escalation resolution by the escalation
231
+ steward (issue #889). Derived from the durable `auto_resolve_escalations` flag, never a
232
+ clobbered process variable.
163
233
  Escalation:
164
234
  type: object
165
235
  description: >-
@@ -239,6 +309,141 @@ components:
239
309
  type: array
240
310
  items:
241
311
  $ref: "#/components/schemas/Escalation"
312
+ AutoResolution:
313
+ type: object
314
+ description: "One escalation-steward auto-resolution record (issue #889) — a reviewable record of an escalation the steward resolved or abstained from."
315
+ additionalProperties: false
316
+ required:
317
+ - id
318
+ - userTaskKey
319
+ - outcome
320
+ - reviewState
321
+ - createdAt
322
+ properties:
323
+ id:
324
+ type: integer
325
+ userTaskKey:
326
+ type: string
327
+ kind:
328
+ type: string
329
+ nullable: true
330
+ elementId:
331
+ type: string
332
+ nullable: true
333
+ subjectType:
334
+ type: string
335
+ nullable: true
336
+ subjectKey:
337
+ type: string
338
+ nullable: true
339
+ subjectUrl:
340
+ type: string
341
+ nullable: true
342
+ processInstanceKey:
343
+ type: string
344
+ nullable: true
345
+ question:
346
+ type: string
347
+ nullable: true
348
+ options:
349
+ type: array
350
+ nullable: true
351
+ items: {}
352
+ outcome:
353
+ type: string
354
+ enum: [resolved, abstained]
355
+ submittedVariables:
356
+ type: object
357
+ nullable: true
358
+ additionalProperties: true
359
+ selectedOption:
360
+ type: string
361
+ nullable: true
362
+ description: >-
363
+ The option value the steward actually selected (derived host-side at record time), so a
364
+ consumer sees WHAT was chosen without decoding `submittedVariables`. Null when abstained /
365
+ not derivable.
366
+ rationale:
367
+ type: string
368
+ nullable: true
369
+ evidence:
370
+ type: string
371
+ nullable: true
372
+ stewardInstanceKey:
373
+ type: string
374
+ nullable: true
375
+ agentJobKey:
376
+ type: string
377
+ nullable: true
378
+ worker:
379
+ type: string
380
+ nullable: true
381
+ model:
382
+ type: string
383
+ nullable: true
384
+ createdAt:
385
+ type: string
386
+ reviewState:
387
+ type: string
388
+ enum: [pending, dismissed, intervened]
389
+ reviewedBy:
390
+ type: string
391
+ nullable: true
392
+ reviewedAt:
393
+ type: string
394
+ nullable: true
395
+ reviewNote:
396
+ type: string
397
+ nullable: true
398
+ AutoResolutionList:
399
+ type: object
400
+ additionalProperties: false
401
+ required:
402
+ - count
403
+ - pendingCount
404
+ - records
405
+ properties:
406
+ count:
407
+ type: integer
408
+ pendingCount:
409
+ type: integer
410
+ description: The number of `pending` (not-yet-reviewed) records — the nav badge count.
411
+ records:
412
+ type: array
413
+ items:
414
+ $ref: "#/components/schemas/AutoResolution"
415
+ AutoResolutionReview:
416
+ type: object
417
+ additionalProperties: false
418
+ required:
419
+ - id
420
+ - action
421
+ properties:
422
+ id:
423
+ type: integer
424
+ description: The auto-resolution record id (from `listAutoResolutions`).
425
+ action:
426
+ type: string
427
+ enum: [dismiss, intervene]
428
+ description: "`dismiss` = reviewed, no action; `intervene` = operator stepped in."
429
+ operator:
430
+ type: string
431
+ description: The reviewing operator's handle (for the audit trail). Optional — defaults to `operator`.
432
+ note:
433
+ type: string
434
+ description: An optional review note (e.g. the intervention guidance).
435
+ AutoResolutionReviewResult:
436
+ type: object
437
+ additionalProperties: false
438
+ required:
439
+ - ok
440
+ properties:
441
+ ok:
442
+ type: boolean
443
+ id:
444
+ type: integer
445
+ reviewState:
446
+ type: string
242
447
  LineagePrView:
243
448
  type: object
244
449
  description: A member PR of a lineage thread (issue #245).
@@ -1686,6 +1891,9 @@ components:
1686
1891
  confirmDefaultBase:
1687
1892
  type: boolean
1688
1893
  description: Acknowledge that `baseBranch` names the repository default branch. See `PlanStartByIssue.confirmDefaultBase`.
1894
+ autoResolveEscalations:
1895
+ type: boolean
1896
+ description: Opt this epic (and every child convergence/merge loop it spawns) into escalation-steward auto-resolution. See `PlanStartByIssue.autoResolveEscalations`.
1689
1897
  EpicSetMemberByUrl:
1690
1898
  type: object
1691
1899
  additionalProperties: false
@@ -1708,6 +1916,9 @@ components:
1708
1916
  confirmDefaultBase:
1709
1917
  type: boolean
1710
1918
  description: Acknowledge landing on the default branch. See `PlanStartByIssue.confirmDefaultBase`.
1919
+ autoResolveEscalations:
1920
+ type: boolean
1921
+ description: Opt this epic (and every child convergence/merge loop it spawns) into escalation-steward auto-resolution. See `PlanStartByIssue.autoResolveEscalations`.
1711
1922
  EpicSetDep:
1712
1923
  description: >-
1713
1924
  One inter-epic dependency edge: the `consumer` epic waits for the `producer` epic to publish
@@ -1826,6 +2037,9 @@ components:
1826
2037
  autoMerge:
1827
2038
  type: boolean
1828
2039
  description: Preferred per-request setting. When false, stop at `converged`; when true, run the merge-loop after convergence only if `NANO_PR_AUTO_MERGE` is enabled. When omitted, the global default applies.
2040
+ autoResolveEscalations:
2041
+ type: boolean
2042
+ description: "Opt this loop's escalations into the escalation steward (issue #889) — it reviews each open escalation every 30 min and submits a recommended choice, recorded reviewably in Auto-resolutions. Default off."
1829
2043
  convergeOnly:
1830
2044
  type: boolean
1831
2045
  deprecated: true
@@ -1850,6 +2064,9 @@ components:
1850
2064
  autoMerge:
1851
2065
  type: boolean
1852
2066
  description: Preferred per-request setting. When false, stop at `converged`; when true, run the merge-loop after convergence only if `NANO_PR_AUTO_MERGE` is enabled. When omitted, the global default applies.
2067
+ autoResolveEscalations:
2068
+ type: boolean
2069
+ description: "Opt this loop's escalations into the escalation steward (issue #889) — it reviews each open escalation every 30 min and submits a recommended choice, recorded reviewably in Auto-resolutions. Default off."
1853
2070
  convergeOnly:
1854
2071
  type: boolean
1855
2072
  deprecated: true
@@ -1907,6 +2124,9 @@ components:
1907
2124
  buffer — and fires any merge-to-default side effect per task — so admission will reject
1908
2125
  (400) unless confirmed with true (no effect for a non-default base). Accepted by the schema
1909
2126
  today but currently has no runtime effect.
2127
+ autoResolveEscalations:
2128
+ type: boolean
2129
+ description: "Opt this epic's (and its child loops') escalations into the escalation steward (issue #889) — it reviews each open escalation every 30 min and submits a recommended choice, recorded reviewably in Auto-resolutions. Default off."
1910
2130
  PlanStartByUrl:
1911
2131
  type: object
1912
2132
  additionalProperties: false
@@ -1939,6 +2159,9 @@ components:
1939
2159
  Accepted by the schema today but currently has no runtime effect. When implemented it
1940
2160
  will be the required acknowledgement when `baseBranch` names the repository default
1941
2161
  branch. See `PlanStartByIssue.confirmDefaultBase`.
2162
+ autoResolveEscalations:
2163
+ type: boolean
2164
+ description: "Opt this epic's (and its child loops') escalations into the escalation steward (issue #889) — it reviews each open escalation every 30 min and submits a recommended choice, recorded reviewably in Auto-resolutions. Default off."
1942
2165
  ReadinessProbe:
1943
2166
  description: >-
1944
2167
  A single durable readiness probe (issue #258, #295) the feature run must satisfy before its
@@ -2518,6 +2741,9 @@ components:
2518
2741
  segment starting with `.` or ending in `.lock`, etc.) is rejected at submit. This pattern
2519
2742
  mirrors the authoritative server-side gate (`isPlausibleBranchName`, app/baseBranch.ts) so the
2520
2743
  documented contract and the door agree.
2744
+ autoResolveEscalations:
2745
+ type: boolean
2746
+ description: "Opt this run's escalations (and any `converge*` connector's enrolled PR) into the escalation steward (issue #889) — it reviews each open escalation every 30 min and submits a recommended choice, recorded reviewably in Auto-resolutions. Default off."
2521
2747
  DeliveryGraphDispatchRepoless:
2522
2748
  description: >-
2523
2749
  The checkout-less dispatch shape (#729): an EXPLICIT `repoless: true` opt-out of repository
@@ -2571,6 +2797,9 @@ components:
2571
2797
  fields are not members of this variant, so supplying either alongside `repoless: true` fails
2572
2798
  `oneOf` matching and is a 400. To provision a repository, use the repository variant (supply
2573
2799
  `repository` + `baseBranch`) and omit `repoless` entirely.
2800
+ autoResolveEscalations:
2801
+ type: boolean
2802
+ description: "Opt this run's escalations (and any `converge*` connector's enrolled PR) into the escalation steward (issue #889) — it reviews each open escalation every 30 min and submits a recommended choice, recorded reviewably in Auto-resolutions. Default off."
2574
2803
  DeliveryGraphDispatchNodeProvisioned:
2575
2804
  description: >-
2576
2805
  The FULLY node-provisioned dispatch shape (#739): supplies NEITHER a run-level `repository`/
@@ -2617,6 +2846,9 @@ components:
2617
2846
  description: >-
2618
2847
  OPTIONAL run-level ISO-8601 SLA for `human` nodes (#505) before they record an `escalated`
2619
2848
  outcome. Absent → the `P1D` default. An invalid duration is rejected at submit.
2849
+ autoResolveEscalations:
2850
+ type: boolean
2851
+ description: "Opt this run's escalations (and any `converge*` connector's enrolled PR) into the escalation steward (issue #889) — it reviews each open escalation every 30 min and submits a recommended choice, recorded reviewably in Auto-resolutions. Default off."
2620
2852
  DeliveryGraphDismissRequest:
2621
2853
  description: >-
2622
2854
  The OPERATOR dismiss request (#520). The cockpit's staged-proposals grid posts the content
@@ -3496,6 +3728,9 @@ components:
3496
3728
  its handles against — the consumer dependency whose published provenance must carry each
3497
3729
  awaited upstream. When present, `blockedOn` desugars to `capability` probes and the
3498
3730
  resolved `pkg@version` is late-bound into the implementation agent's brief.
3731
+ autoResolveEscalations:
3732
+ type: boolean
3733
+ description: "Opt this run's escalations into the escalation steward (issue #889) — it reviews each open escalation every 30 min and submits a recommended choice, recorded reviewably in Auto-resolutions. Default off."
3499
3734
  FeatureStartByUrl:
3500
3735
  type: object
3501
3736
  additionalProperties: false
@@ -3552,6 +3787,9 @@ components:
3552
3787
  type: string
3553
3788
  minLength: 1
3554
3789
  description: OPTIONAL package the `blockedOn` handles resolve against. See `FeatureStartByIssue.consumerPackage`.
3790
+ autoResolveEscalations:
3791
+ type: boolean
3792
+ description: "Opt this run's escalations into the escalation steward (issue #889). See `FeatureStartByIssue.autoResolveEscalations`."
3555
3793
  MessageResult:
3556
3794
  type: object
3557
3795
  description: The result of publishing a message / answering an escalation. Shape varies by message
@@ -3889,6 +4127,109 @@ paths:
3889
4127
  application/json:
3890
4128
  schema:
3891
4129
  $ref: "#/components/schemas/ErrorBody"
4130
+ /auto-resolutions:
4131
+ get:
4132
+ operationId: listAutoResolutions
4133
+ summary: List the escalation steward's auto-resolution records (reviewable), newest first.
4134
+ description: >-
4135
+ The durable, reviewable record of every escalation the opt-in steward (issue #889) resolved
4136
+ or abstained from. Filter by `reviewState` (`pending` | `dismissed` | `intervened`),
4137
+ `subject` (a PR/feature/plan/delivery key) or `kind` (an escalation element id or label).
4138
+ Each record carries the question, the options offered, the submitted variables, the
4139
+ rationale and evidence, the steward provenance, and its operator review state.
4140
+ security:
4141
+ - hookSecret: []
4142
+ - {}
4143
+ parameters:
4144
+ - name: reviewState
4145
+ in: query
4146
+ required: false
4147
+ schema:
4148
+ type: string
4149
+ enum: [pending, dismissed, intervened]
4150
+ description: Filter to one review state (the Auto-resolutions view defaults to `pending`).
4151
+ - name: subject
4152
+ in: query
4153
+ required: false
4154
+ schema:
4155
+ type: string
4156
+ description: Filter to one subject key (e.g. `owner/repo#123` or a delivery run key).
4157
+ - name: kind
4158
+ in: query
4159
+ required: false
4160
+ schema:
4161
+ type: string
4162
+ description: Filter to one escalation kind (an element id or kind label).
4163
+ responses:
4164
+ "200":
4165
+ description: The auto-resolution records.
4166
+ content:
4167
+ application/json:
4168
+ schema:
4169
+ $ref: "#/components/schemas/AutoResolutionList"
4170
+ "401":
4171
+ description: Missing/invalid shared secret (only when NANO_PR_WEBHOOK_SECRET is set).
4172
+ content:
4173
+ application/json:
4174
+ schema:
4175
+ $ref: "#/components/schemas/ErrorBody"
4176
+ /auto-resolutions/review:
4177
+ post:
4178
+ operationId: reviewAutoResolution
4179
+ summary: Mark an auto-resolution record dismissed (reviewed, no action) or intervened (operator stepped in).
4180
+ description: >-
4181
+ The Auto-resolutions view's review affordance (issue #889). `dismiss` marks the record
4182
+ reviewed with no further action; `intervene` marks it and records the operator's guidance for
4183
+ stepping in after the fact. Both fold `review_state` off `pending` (idempotently — an
4184
+ already-reviewed record is left unchanged) and record who reviewed it, when, and an optional
4185
+ note. Reopening the completed user task is NOT required.
4186
+ requestBody:
4187
+ required: true
4188
+ content:
4189
+ application/json:
4190
+ schema:
4191
+ # BEGIN generated:mcp-body source=#/components/schemas/AutoResolutionReview (scripts/inline-mcp-bodies.ts — do not hand-edit)
4192
+ type: object
4193
+ additionalProperties: false
4194
+ required:
4195
+ - id
4196
+ - action
4197
+ properties:
4198
+ id:
4199
+ type: integer
4200
+ description: The auto-resolution record id (from `listAutoResolutions`).
4201
+ action:
4202
+ type: string
4203
+ enum:
4204
+ - dismiss
4205
+ - intervene
4206
+ description: '`dismiss` = reviewed, no action; `intervene` = operator stepped in.'
4207
+ operator:
4208
+ type: string
4209
+ description: The reviewing operator's handle (for the audit trail). Optional — defaults to `operator`.
4210
+ note:
4211
+ type: string
4212
+ description: An optional review note (e.g. the intervention guidance).
4213
+ # END generated:mcp-body
4214
+ responses:
4215
+ "200":
4216
+ description: The record was reviewed.
4217
+ content:
4218
+ application/json:
4219
+ schema:
4220
+ $ref: "#/components/schemas/AutoResolutionReviewResult"
4221
+ "400":
4222
+ description: The request was malformed (missing id/action, or an unknown action).
4223
+ content:
4224
+ application/json:
4225
+ schema:
4226
+ $ref: "#/components/schemas/ErrorBody"
4227
+ "404":
4228
+ description: No pending record with that id (already reviewed, or unknown).
4229
+ content:
4230
+ application/json:
4231
+ schema:
4232
+ $ref: "#/components/schemas/ErrorBody"
3892
4233
  /escalations:
3893
4234
  get:
3894
4235
  operationId: listEscalations
@@ -4590,6 +4931,9 @@ paths:
4590
4931
  autoMerge:
4591
4932
  type: boolean
4592
4933
  description: Preferred per-request setting. When false, stop at `converged`; when true, run the merge-loop after convergence only if `NANO_PR_AUTO_MERGE` is enabled. When omitted, the global default applies.
4934
+ autoResolveEscalations:
4935
+ type: boolean
4936
+ description: "Opt this loop's escalations into the escalation steward (issue #889) — it reviews each open escalation every 30 min and submits a recommended choice, recorded reviewably in Auto-resolutions. Default off."
4593
4937
  convergeOnly:
4594
4938
  type: boolean
4595
4939
  deprecated: true
@@ -4613,6 +4957,9 @@ paths:
4613
4957
  autoMerge:
4614
4958
  type: boolean
4615
4959
  description: Preferred per-request setting. When false, stop at `converged`; when true, run the merge-loop after convergence only if `NANO_PR_AUTO_MERGE` is enabled. When omitted, the global default applies.
4960
+ autoResolveEscalations:
4961
+ type: boolean
4962
+ description: "Opt this loop's escalations into the escalation steward (issue #889) — it reviews each open escalation every 30 min and submits a recommended choice, recorded reviewably in Auto-resolutions. Default off."
4616
4963
  convergeOnly:
4617
4964
  type: boolean
4618
4965
  deprecated: true
@@ -4665,6 +5012,9 @@ paths:
4665
5012
  confirmDefaultBase:
4666
5013
  type: boolean
4667
5014
  description: 'Reserved for a later ADR 0003 admission slice — NOT yet enforced in this slice (B0). When implemented it will be the required acknowledgement when `baseBranch` names the repository default branch: targeting the default lands every task directly on it with no integration buffer — and fires any merge-to-default side effect per task — so admission will reject (400) unless confirmed with true (no effect for a non-default base). Accepted by the schema today but currently has no runtime effect.'
5015
+ autoResolveEscalations:
5016
+ type: boolean
5017
+ description: "Opt this epic's (and its child loops') escalations into the escalation steward (issue #889) — it reviews each open escalation every 30 min and submits a recommended choice, recorded reviewably in Auto-resolutions. Default off."
4668
5018
  - type: object
4669
5019
  additionalProperties: false
4670
5020
  required:
@@ -4686,6 +5036,9 @@ paths:
4686
5036
  confirmDefaultBase:
4687
5037
  type: boolean
4688
5038
  description: Reserved for a later ADR 0003 admission slice — NOT yet enforced in this slice (B0). Accepted by the schema today but currently has no runtime effect. When implemented it will be the required acknowledgement when `baseBranch` names the repository default branch. See `PlanStartByIssue.confirmDefaultBase`.
5039
+ autoResolveEscalations:
5040
+ type: boolean
5041
+ description: "Opt this epic's (and its child loops') escalations into the escalation steward (issue #889) — it reviews each open escalation every 30 min and submits a recommended choice, recorded reviewably in Auto-resolutions. Default off."
4689
5042
  type: object
4690
5043
  # END generated:mcp-body
4691
5044
  responses:
@@ -4755,6 +5108,9 @@ paths:
4755
5108
  confirmDefaultBase:
4756
5109
  type: boolean
4757
5110
  description: Acknowledge that `baseBranch` names the repository default branch. See `PlanStartByIssue.confirmDefaultBase`.
5111
+ autoResolveEscalations:
5112
+ type: boolean
5113
+ description: Opt this epic (and every child convergence/merge loop it spawns) into escalation-steward auto-resolution. See `PlanStartByIssue.autoResolveEscalations`.
4758
5114
  - type: object
4759
5115
  additionalProperties: false
4760
5116
  required:
@@ -4776,6 +5132,9 @@ paths:
4776
5132
  confirmDefaultBase:
4777
5133
  type: boolean
4778
5134
  description: Acknowledge landing on the default branch. See `PlanStartByIssue.confirmDefaultBase`.
5135
+ autoResolveEscalations:
5136
+ type: boolean
5137
+ description: Opt this epic (and every child convergence/merge loop it spawns) into escalation-steward auto-resolution. See `PlanStartByIssue.autoResolveEscalations`.
4779
5138
  deps:
4780
5139
  type: array
4781
5140
  description: The inter-epic dependency edges. Each declares `consumer` waits for `producer` to publish the `{ package, capabilityRef }` capability. Both endpoints must name epics in `epics`. Omit or pass `[]` for a set of independent (root) epics.
@@ -6319,6 +6678,9 @@ paths:
6319
6678
  type: string
6320
6679
  minLength: 1
6321
6680
  description: OPTIONAL npm package name (e.g. `@nanobpm/engine-wasm`) the `blockedOn` shorthand resolves its handles against — the consumer dependency whose published provenance must carry each awaited upstream. When present, `blockedOn` desugars to `capability` probes and the resolved `pkg@version` is late-bound into the implementation agent's brief.
6681
+ autoResolveEscalations:
6682
+ type: boolean
6683
+ description: "Opt this run's escalations into the escalation steward (issue #889) — it reviews each open escalation every 30 min and submits a recommended choice, recorded reviewably in Auto-resolutions. Default off."
6322
6684
  - type: object
6323
6685
  additionalProperties: false
6324
6686
  required:
@@ -6467,6 +6829,9 @@ paths:
6467
6829
  type: string
6468
6830
  minLength: 1
6469
6831
  description: OPTIONAL package the `blockedOn` handles resolve against. See `FeatureStartByIssue.consumerPackage`.
6832
+ autoResolveEscalations:
6833
+ type: boolean
6834
+ description: "Opt this run's escalations into the escalation steward (issue #889). See `FeatureStartByIssue.autoResolveEscalations`."
6470
6835
  type: object
6471
6836
  # END generated:mcp-body
6472
6837
  responses:
@@ -120,6 +120,11 @@ export default defineOperation("dispatchDeliveryGraph", async ({ body }, app) =>
120
120
  if (parsed.value !== undefined) timeouts[field] = parsed.value;
121
121
  }
122
122
 
123
+ // Opt-in escalation steward (issue #889) — when the dispatch body sets `autoResolveEscalations`,
124
+ // the run's escalations (and any `converge*` connector's enrolled PR) become eligible for steward
125
+ // auto-resolution. Default off.
126
+ const autoResolveEscalations = body && typeof body === "object" && "autoResolveEscalations" in body && body.autoResolveEscalations === true;
127
+
123
128
  // Host-git provisioning override (#684/#686) — the OPTIONAL `owner/repo` + base branch the run's
124
129
  // `agent` nodes implement against. When both are present the runner seeds the canonical
125
130
  // `io.nanobpm.agentTask.repository` isolation envelope (`repoEnvelopeVars`) onto every agent cell's
@@ -267,7 +272,7 @@ export default defineOperation("dispatchDeliveryGraph", async ({ body }, app) =>
267
272
  }
268
273
  }
269
274
 
270
- const dispatched = await dispatchDeliveryGraphRun(app, graph, { runKey: dispatchRunKey, title: proposal.title, repository, baseBranch, repoless, expectedDigest: digest, ...timeouts });
275
+ const dispatched = await dispatchDeliveryGraphRun(app, graph, { runKey: dispatchRunKey, title: proposal.title, repository, baseBranch, repoless, expectedDigest: digest, autoResolveEscalations, ...timeouts });
271
276
  if (!dispatched.ok) {
272
277
  app.log.warn("dispatch-delivery-graph refused: compile", { digest, errors: dispatched.errors.length });
273
278
  const outBody: DeliveryGraphTextResult = {
@@ -8,7 +8,7 @@ import { noopLog } from "../test/log.ts";
8
8
  import { withTrackingViews } from "../test/trackingViews.ts";
9
9
  import handler from "./listActivePrs.ts";
10
10
 
11
- function memApp(rows: any[], userTaskRows: any[] = []): AppApi {
11
+ function memApp(rows: any[], userTaskRows: any[] = [], deliveryRows: any[] = []): AppApi {
12
12
  const table = (name: string) => {
13
13
  if (name === "user_tasks") {
14
14
  return {
@@ -20,6 +20,13 @@ function memApp(rows: any[], userTaskRows: any[] = []): AppApi {
20
20
  },
21
21
  };
22
22
  }
23
+ if (name === "delivery_graph_runs") {
24
+ return {
25
+ async all() {
26
+ return deliveryRows;
27
+ },
28
+ };
29
+ }
23
30
  return {
24
31
  async all() {
25
32
  return rows;
@@ -115,3 +122,55 @@ test("shared-secret guard rejects a missing secret when configured", async () =>
115
122
  else process.env["NANO_PR_WEBHOOK_SECRET"] = prev;
116
123
  }
117
124
  });
125
+
126
+ test("surfaces each in-flight delivery run with its durable escalation-steward opt-in (issue #889 §1)", async () => {
127
+ // `/status` must show the opt-in for each PR AND each delivery. A terminal (`done`) run drops out;
128
+ // a live run carries its durable `auto_resolve_escalations` flag (never a clobbered process var) and
129
+ // its open escalation pointer (derived from the SAME `user_tasks` read model, subject_type
130
+ // "delivery", keyed by run_key).
131
+ const app = memApp(
132
+ [],
133
+ [
134
+ {
135
+ user_task_key: "ut-d1",
136
+ element_id: "delivery-human-taskish",
137
+ kind_label: "Delivery approval",
138
+ subject_type: "delivery",
139
+ subject_key: "run-live",
140
+ subject_title: "Ship it",
141
+ subject_url: null,
142
+ question: "approve the release?",
143
+ process_key: "pk-live",
144
+ form_key: null,
145
+ created_at: "2026-03-02",
146
+ updated_at: "2026-03-02",
147
+ },
148
+ ],
149
+ [
150
+ { run_key: "run-live", process_key: "pk-live", status: "running", phase: "Running", title: "Ship it", auto_resolve_escalations: 1, updated_at: "2026-03-02" },
151
+ { run_key: "run-done", process_key: "pk-done", status: "done", phase: "Done", title: "Old", auto_resolve_escalations: 0, updated_at: "2026-03-01" },
152
+ ],
153
+ );
154
+ const res = (await handler(input(), app)) as any;
155
+ assertEquals(res.status, 200);
156
+ assertEquals(res.body.deliveries.length, 1, "a terminal (done) run drops out of the active list");
157
+ const d = res.body.deliveries[0];
158
+ assertEquals(d.runKey, "run-live");
159
+ assertEquals(d.autoResolveEscalations, true, "the durable opt-in flag is surfaced for the delivery");
160
+ assertEquals(d.processKey, "pk-live");
161
+ assertEquals(d.openEscalation, { userTaskKey: "ut-d1", kind: "delivery-human-taskish", summary: "approve the release?" });
162
+ });
163
+
164
+ test("a delivery run with no opt-in row flag surfaces autoResolveEscalations false and a null escalation", async () => {
165
+ const app = memApp(
166
+ [],
167
+ [],
168
+ [{ run_key: "run-plain", process_key: null, status: "running", phase: null, title: null, auto_resolve_escalations: 0, updated_at: "2026-03-03" }],
169
+ );
170
+ const res = (await handler(input(), app)) as any;
171
+ assertEquals(res.status, 200);
172
+ assertEquals(res.body.deliveries.length, 1);
173
+ assertEquals(res.body.deliveries[0].autoResolveEscalations, false);
174
+ assertEquals(res.body.deliveries[0].openEscalation, null);
175
+ assertEquals(res.body.deliveries[0].processKey, null, "a launch-in-flight run has no stamped process_key yet");
176
+ });
@@ -7,7 +7,7 @@
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
9
 
10
- import { activePrs } from "../app/service.ts";
10
+ import { activeDeliveries, activePrs } from "../app/service.ts";
11
11
  import { envVar } from "../app/version.ts";
12
12
  import { defineOperation } from "../nano-generated/operations.ts";
13
13
 
@@ -20,6 +20,6 @@ export default defineOperation("listActivePrs", async ({ req }, app) => {
20
20
  app.log.warn("listActivePrs rejected: missing/invalid shared secret");
21
21
  return { status: 401, body: { error: "unauthorized" } };
22
22
  }
23
- const prs = await activePrs(app.data);
24
- return { status: 200, body: { count: prs.length, prs } };
23
+ const [prs, deliveries] = await Promise.all([activePrs(app.data), activeDeliveries(app.data)]);
24
+ return { status: 200, body: { count: prs.length, prs, deliveries } };
25
25
  });