@nanobpm/nano-workforce 0.201.5 → 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 (83) hide show
  1. package/CHANGELOG.md +12 -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/README.md +48 -0
  8. package/app/agentic/claim-registry.test.ts +61 -0
  9. package/app/agentic/claim-registry.ts +35 -1
  10. package/app/agentic/families/claim.family.test.ts +216 -0
  11. package/app/agentic/families/claim.family.ts +100 -4
  12. package/app/agentic/vocab/agent-sla-boundary.test.ts +6 -1
  13. package/app/contracts.ts +31 -0
  14. package/app/deliveryGraphDispatch.ts +6 -0
  15. package/app/deliveryGraphReadModel.test.ts +2 -2
  16. package/app/deliveryGraphRun.ts +18 -0
  17. package/app/deliveryRunner.ts +9 -1
  18. package/app/escalationSteward.test.ts +452 -0
  19. package/app/escalationSteward.ts +537 -0
  20. package/app/escalationStewardPoll.test.ts +123 -0
  21. package/app/feature.test.ts +16 -0
  22. package/app/feature.ts +11 -0
  23. package/app/featureDelivery.test.ts +23 -0
  24. package/app/mcpToolSurface.ts +17 -1
  25. package/app/plan.ts +49 -6
  26. package/app/planLowering.test.ts +26 -1
  27. package/app/planLowering.ts +5 -0
  28. package/app/promotionPoll.test.ts +19 -0
  29. package/app/pullRequestReadModel.test.ts +1 -1
  30. package/app/service.test.ts +36 -0
  31. package/app/service.ts +139 -5
  32. package/db/migrations/120_escalation_auto_resolutions.sql +229 -0
  33. package/db/migrations/121_delivery_graph_read_model_auto_resolve.sql +54 -0
  34. package/db/migrations/122_pull_requests_read_model_auto_resolve.sql +56 -0
  35. package/db/migrations/123_escalation_auto_resolutions_selected_option.sql +47 -0
  36. package/db/migrations/124_admitted_epics_auto_resolve.sql +21 -0
  37. package/docs/agent-guide.md +69 -30
  38. package/docs/agentic-cockpit.md +34 -0
  39. package/docs/mcp-runbook.md +2 -0
  40. package/e2e/escalation-steward.e2e.ts +211 -0
  41. package/nano.app.json +10 -1
  42. package/openapi.yaml +365 -0
  43. package/operations/dispatchDeliveryGraph.ts +6 -1
  44. package/operations/listActivePrs.test.ts +60 -1
  45. package/operations/listActivePrs.ts +3 -3
  46. package/operations/listAutoResolutions.ts +83 -0
  47. package/operations/reviewAutoResolution.test.ts +137 -0
  48. package/operations/reviewAutoResolution.ts +42 -0
  49. package/operations/startConvergenceLoop.ts +5 -1
  50. package/operations/startEpicSet.admission.integration.test.ts +2 -2
  51. package/operations/startEpicSet.ts +8 -2
  52. package/operations/startFeature.ts +5 -0
  53. package/operations/startPlanFanout.ts +4 -1
  54. package/package.json +1 -1
  55. package/pages/_nav.json +12 -0
  56. package/pages/auto-resolutions.page.json +191 -0
  57. package/pages/board.page.json +17 -0
  58. package/pages/cockpit.page.json +17 -0
  59. package/pages/delivery-graph-detail.page.json +17 -0
  60. package/pages/delivery-graphs/staged.mount.js +17 -5
  61. package/pages/delivery-graphs.page.json +17 -0
  62. package/pages/epic-detail.page.json +17 -0
  63. package/pages/epic.page.json +19 -1
  64. package/pages/feature.page.json +19 -1
  65. package/pages/home.page.json +22 -0
  66. package/pages/lineage.page.json +17 -0
  67. package/pages/mcp.page.json +17 -0
  68. package/pages/overview.page.json +17 -0
  69. package/pages/tasks.page.json +17 -0
  70. package/pages/velocity.page.json +17 -0
  71. package/resources/processes/escalation-steward.bpmn +162 -0
  72. package/resources/prompts/escalation-steward.md +87 -0
  73. package/test/delivery-graphs-staged-embed.test.ts +13 -0
  74. package/test/derivation-parity/derivation-parity.test.ts +1 -0
  75. package/test/derivation-parity/flows.ts +12 -0
  76. package/workers/converge-feature/worker.ts +6 -1
  77. package/workers/delivery-connector/worker.test.ts +72 -1
  78. package/workers/delivery-connector/worker.ts +53 -2
  79. package/workers/escalation-steward-collect/worker.test.ts +180 -0
  80. package/workers/escalation-steward-collect/worker.ts +99 -0
  81. package/workers/escalation-steward-record/worker.test.ts +740 -0
  82. package/workers/escalation-steward-record/worker.ts +428 -0
  83. package/workers/record-wave/worker.ts +5 -1
@@ -49,6 +49,23 @@
49
49
  "hideWhenZero": true
50
50
  }
51
51
  },
52
+ {
53
+ "label": "Auto-resolutions",
54
+ "page": "auto-resolutions",
55
+ "badge": {
56
+ "source": "app",
57
+ "table": "escalation_auto_resolutions__read_model",
58
+ "filter": [
59
+ {
60
+ "field": "review_state",
61
+ "eq": "pending"
62
+ }
63
+ ],
64
+ "tone": "warn",
65
+ "refreshMs": 5000,
66
+ "hideWhenZero": true
67
+ }
68
+ },
52
69
  {
53
70
  "label": "Cockpit",
54
71
  "page": "cockpit"
@@ -49,6 +49,23 @@
49
49
  "hideWhenZero": true
50
50
  }
51
51
  },
52
+ {
53
+ "label": "Auto-resolutions",
54
+ "page": "auto-resolutions",
55
+ "badge": {
56
+ "source": "app",
57
+ "table": "escalation_auto_resolutions__read_model",
58
+ "filter": [
59
+ {
60
+ "field": "review_state",
61
+ "eq": "pending"
62
+ }
63
+ ],
64
+ "tone": "warn",
65
+ "refreshMs": 5000,
66
+ "hideWhenZero": true
67
+ }
68
+ },
52
69
  {
53
70
  "label": "Cockpit",
54
71
  "page": "cockpit"
@@ -113,6 +113,7 @@ function dispatchControl(p, pending) {
113
113
  if (isPending(pending, "dispatch", p.digest)) {
114
114
  const repository = typeof pending.repository === "string" ? pending.repository : "";
115
115
  const baseBranch = typeof pending.baseBranch === "string" ? pending.baseBranch : "";
116
+ const autoResolve = pending.autoResolveEscalations === true;
116
117
  const mode = pending.mode;
117
118
  const provDisabled = mode === "fallback" ? "" : " disabled";
118
119
  return `<span class="confirm" data-confirm="dispatch">
@@ -123,6 +124,7 @@ function dispatchControl(p, pending) {
123
124
  <span class="confirm-msg">Use node repositories sends no run-level repository settings; the server validates that every agent node is provisioned. A run-level repository is a fallback for nodes without their own repository, not an override of node declarations. Checkout-less explicitly opts out of repository provisioning.</span>
124
125
  <label class="confirm-field">Repository <input class="confirm-input" type="text" data-dispatch-repository="${esc(p.digest)}" value="${esc(repository)}" placeholder="owner/repo" aria-label="Repository (owner/repo)"${provDisabled} /></label>
125
126
  <label class="confirm-field">Base branch <input class="confirm-input" type="text" data-dispatch-base="${esc(p.digest)}" value="${esc(baseBranch)}" placeholder="main" aria-label="Base branch"${provDisabled} /></label>
127
+ <label class="confirm-check"><input type="checkbox" data-dispatch-autoresolve="${esc(p.digest)}"${autoResolve ? " checked" : ""} /> Automatically resolve escalations (the escalation steward answers this run's eligible escalations; every automatic decision stays reviewable)</label>
126
128
  <button class="btn btn-primary" type="button" data-dispatch-confirm="${esc(p.digest)}">Confirm dispatch</button>
127
129
  <button class="btn btn-ghost" type="button" data-dispatch-cancel="${esc(p.digest)}">Cancel</button>
128
130
  </span>`;
@@ -458,7 +460,7 @@ export function mountStagedProposals(host, config = {}) {
458
460
  function openConfirm(kind, digest, name) {
459
461
  const staged = typeof digest === "string" ? digest.trim() : "";
460
462
  if (staged === "") return;
461
- pending = { kind, digest: staged, name: typeof name === "string" ? name : "", repository: "", baseBranch: "", mode: "nodes" };
463
+ pending = { kind, digest: staged, name: typeof name === "string" ? name : "", repository: "", baseBranch: "", mode: "nodes", autoResolveEscalations: false };
462
464
  rerender();
463
465
  // Move focus into the name input so the operator can type immediately (best-effort; not all hosts
464
466
  // implement focus()).
@@ -514,8 +516,16 @@ export function mountStagedProposals(host, config = {}) {
514
516
  }
515
517
  });
516
518
 
517
- // Preserve fallback text across mode changes, but only enable and submit it in fallback mode.
519
+ // Preserve fallback text across mode changes, but only enable and submit it in fallback mode. Also
520
+ // track the "Automatically resolve escalations" checkbox (a checkbox fires `change`, not `input`).
518
521
  listEl.addEventListener("change", (ev) => {
522
+ if (pending != null && pending.kind === "dispatch") {
523
+ const autoResolve = ev.target && ev.target.closest ? ev.target.closest("[data-dispatch-autoresolve]") : null;
524
+ if (autoResolve && autoResolve.getAttribute("data-dispatch-autoresolve") === pending.digest) {
525
+ pending.autoResolveEscalations = autoResolve.checked === true;
526
+ return;
527
+ }
528
+ }
519
529
  const choice = ev.target && ev.target.closest ? ev.target.closest("[data-dispatch-mode]") : null;
520
530
  if (choice && choice.checked && pending != null && pending.kind === "dispatch" &&
521
531
  choice.getAttribute("data-dispatch-mode") === pending.digest &&
@@ -562,10 +572,12 @@ export function mountStagedProposals(host, config = {}) {
562
572
  return;
563
573
  }
564
574
  // Construct only the selected mode's fields: disabled fallback text must never leak into node
565
- // or checkout-less dispatches, and repoless must never be inferred from blank fields.
575
+ // or checkout-less dispatches, and repoless must never be inferred from blank fields. The
576
+ // escalation-steward opt-in is mode-independent — include it whenever the operator checked it.
577
+ const autoResolve = pending.autoResolveEscalations === true ? { autoResolveEscalations: true } : {};
566
578
  const payload = mode === "fallback"
567
- ? { digest, repository, baseBranch }
568
- : mode === "repoless" ? { digest, repoless: true } : { digest };
579
+ ? { digest, repository, baseBranch, ...autoResolve }
580
+ : mode === "repoless" ? { digest, repoless: true, ...autoResolve } : { digest, ...autoResolve };
569
581
  closeConfirm();
570
582
  doDispatch(payload);
571
583
  return;
@@ -49,6 +49,23 @@
49
49
  "hideWhenZero": true
50
50
  }
51
51
  },
52
+ {
53
+ "label": "Auto-resolutions",
54
+ "page": "auto-resolutions",
55
+ "badge": {
56
+ "source": "app",
57
+ "table": "escalation_auto_resolutions__read_model",
58
+ "filter": [
59
+ {
60
+ "field": "review_state",
61
+ "eq": "pending"
62
+ }
63
+ ],
64
+ "tone": "warn",
65
+ "refreshMs": 5000,
66
+ "hideWhenZero": true
67
+ }
68
+ },
52
69
  {
53
70
  "label": "Cockpit",
54
71
  "page": "cockpit"
@@ -49,6 +49,23 @@
49
49
  "hideWhenZero": true
50
50
  }
51
51
  },
52
+ {
53
+ "label": "Auto-resolutions",
54
+ "page": "auto-resolutions",
55
+ "badge": {
56
+ "source": "app",
57
+ "table": "escalation_auto_resolutions__read_model",
58
+ "filter": [
59
+ {
60
+ "field": "review_state",
61
+ "eq": "pending"
62
+ }
63
+ ],
64
+ "tone": "warn",
65
+ "refreshMs": 5000,
66
+ "hideWhenZero": true
67
+ }
68
+ },
52
69
  {
53
70
  "label": "Cockpit",
54
71
  "page": "cockpit"
@@ -49,6 +49,23 @@
49
49
  "hideWhenZero": true
50
50
  }
51
51
  },
52
+ {
53
+ "label": "Auto-resolutions",
54
+ "page": "auto-resolutions",
55
+ "badge": {
56
+ "source": "app",
57
+ "table": "escalation_auto_resolutions__read_model",
58
+ "filter": [
59
+ {
60
+ "field": "review_state",
61
+ "eq": "pending"
62
+ }
63
+ ],
64
+ "tone": "warn",
65
+ "refreshMs": 5000,
66
+ "hideWhenZero": true
67
+ }
68
+ },
52
69
  {
53
70
  "label": "Cockpit",
54
71
  "page": "cockpit"
@@ -89,7 +106,8 @@
89
106
  { "key": "issue", "label": "owner/repo#123 or a GitHub issue URL", "type": "text", "required": true, "requiredMessage": "An issue reference is required (owner/repo#123 or an issue URL)" },
90
107
  { "key": "baseBranch", "label": "Base branch: e.g. epic/agent-protocol to land the whole epic on an integration branch. A missing epic/* branch is auto-created off default HEAD; a non-epic/* branch must already exist.", "type": "text", "required": true, "requiredMessage": "Name the branch the PR targets (e.g. main or epic/agent-protocol)" },
91
108
  { "key": "confirmDefaultBase", "label": "Confirm landing on the default branch \u2014 required only when the base above IS the repository default (every task lands directly on it, with any merge-to-default side effect firing per task)", "type": "checkbox" },
92
- { "key": "allowSharedBase", "label": "Allow sharing a custom integration branch with another active epic \u2014 required only when another in-flight epic already targets this same custom base", "type": "checkbox" }
109
+ { "key": "allowSharedBase", "label": "Allow sharing a custom integration branch with another active epic \u2014 required only when another in-flight epic already targets this same custom base", "type": "checkbox" },
110
+ { "key": "autoResolveEscalations", "label": "Automatically resolve escalations \u2014 the opt-in escalation steward reviews this epic's (and its child loops') open escalations every 30 min and submits a recommended choice, recorded reviewably in Auto-resolutions", "type": "checkbox" }
93
111
  ]
94
112
  }
95
113
  },
@@ -49,6 +49,23 @@
49
49
  "hideWhenZero": true
50
50
  }
51
51
  },
52
+ {
53
+ "label": "Auto-resolutions",
54
+ "page": "auto-resolutions",
55
+ "badge": {
56
+ "source": "app",
57
+ "table": "escalation_auto_resolutions__read_model",
58
+ "filter": [
59
+ {
60
+ "field": "review_state",
61
+ "eq": "pending"
62
+ }
63
+ ],
64
+ "tone": "warn",
65
+ "refreshMs": 5000,
66
+ "hideWhenZero": true
67
+ }
68
+ },
52
69
  {
53
70
  "label": "Cockpit",
54
71
  "page": "cockpit"
@@ -93,7 +110,8 @@
93
110
  { "key": "humanApproval", "label": "Human approval before merge \u2014 a converged PR waits in Tasks for you to approve the merge or request changes (only applies when Auto-merge is on)", "type": "checkbox" },
94
111
  { "key": "autoMerge", "label": "Auto-merge \u2014 after convergence, drive the merge-loop (only applies when Converge is on)", "type": "checkbox" },
95
112
  { "key": "confirmDefaultBase", "label": "Confirm landing on the default branch \u2014 required only when the base above IS the repository default", "type": "checkbox" },
96
- { "key": "allowSharedBase", "label": "Allow sharing a custom integration branch with another active epic", "type": "checkbox" }
113
+ { "key": "allowSharedBase", "label": "Allow sharing a custom integration branch with another active epic", "type": "checkbox" },
114
+ { "key": "autoResolveEscalations", "label": "Automatically resolve escalations \u2014 the opt-in escalation steward reviews this run's open escalations every 30 min and submits a recommended choice, recorded reviewably in Auto-resolutions", "type": "checkbox" }
97
115
  ]
98
116
  }
99
117
  },
@@ -49,6 +49,23 @@
49
49
  "hideWhenZero": true
50
50
  }
51
51
  },
52
+ {
53
+ "label": "Auto-resolutions",
54
+ "page": "auto-resolutions",
55
+ "badge": {
56
+ "source": "app",
57
+ "table": "escalation_auto_resolutions__read_model",
58
+ "filter": [
59
+ {
60
+ "field": "review_state",
61
+ "eq": "pending"
62
+ }
63
+ ],
64
+ "tone": "warn",
65
+ "refreshMs": 5000,
66
+ "hideWhenZero": true
67
+ }
68
+ },
52
69
  {
53
70
  "label": "Cockpit",
54
71
  "page": "cockpit"
@@ -108,6 +125,11 @@
108
125
  "key": "autoMerge",
109
126
  "label": "Auto-merge \u2014 after convergence, drive the merge-loop",
110
127
  "type": "checkbox"
128
+ },
129
+ {
130
+ "key": "autoResolveEscalations",
131
+ "label": "Automatically resolve escalations \u2014 the opt-in escalation steward reviews this loop's open escalations every 30 min and submits a recommended choice, recorded reviewably in Auto-resolutions",
132
+ "type": "checkbox"
111
133
  }
112
134
  ]
113
135
  }
@@ -49,6 +49,23 @@
49
49
  "hideWhenZero": true
50
50
  }
51
51
  },
52
+ {
53
+ "label": "Auto-resolutions",
54
+ "page": "auto-resolutions",
55
+ "badge": {
56
+ "source": "app",
57
+ "table": "escalation_auto_resolutions__read_model",
58
+ "filter": [
59
+ {
60
+ "field": "review_state",
61
+ "eq": "pending"
62
+ }
63
+ ],
64
+ "tone": "warn",
65
+ "refreshMs": 5000,
66
+ "hideWhenZero": true
67
+ }
68
+ },
52
69
  {
53
70
  "label": "Cockpit",
54
71
  "page": "cockpit"
@@ -49,6 +49,23 @@
49
49
  "hideWhenZero": true
50
50
  }
51
51
  },
52
+ {
53
+ "label": "Auto-resolutions",
54
+ "page": "auto-resolutions",
55
+ "badge": {
56
+ "source": "app",
57
+ "table": "escalation_auto_resolutions__read_model",
58
+ "filter": [
59
+ {
60
+ "field": "review_state",
61
+ "eq": "pending"
62
+ }
63
+ ],
64
+ "tone": "warn",
65
+ "refreshMs": 5000,
66
+ "hideWhenZero": true
67
+ }
68
+ },
52
69
  {
53
70
  "label": "Cockpit",
54
71
  "page": "cockpit"
@@ -49,6 +49,23 @@
49
49
  "hideWhenZero": true
50
50
  }
51
51
  },
52
+ {
53
+ "label": "Auto-resolutions",
54
+ "page": "auto-resolutions",
55
+ "badge": {
56
+ "source": "app",
57
+ "table": "escalation_auto_resolutions__read_model",
58
+ "filter": [
59
+ {
60
+ "field": "review_state",
61
+ "eq": "pending"
62
+ }
63
+ ],
64
+ "tone": "warn",
65
+ "refreshMs": 5000,
66
+ "hideWhenZero": true
67
+ }
68
+ },
52
69
  {
53
70
  "label": "Cockpit",
54
71
  "page": "cockpit"
@@ -49,6 +49,23 @@
49
49
  "hideWhenZero": true
50
50
  }
51
51
  },
52
+ {
53
+ "label": "Auto-resolutions",
54
+ "page": "auto-resolutions",
55
+ "badge": {
56
+ "source": "app",
57
+ "table": "escalation_auto_resolutions__read_model",
58
+ "filter": [
59
+ {
60
+ "field": "review_state",
61
+ "eq": "pending"
62
+ }
63
+ ],
64
+ "tone": "warn",
65
+ "refreshMs": 5000,
66
+ "hideWhenZero": true
67
+ }
68
+ },
52
69
  {
53
70
  "label": "Cockpit",
54
71
  "page": "cockpit"
@@ -49,6 +49,23 @@
49
49
  "hideWhenZero": true
50
50
  }
51
51
  },
52
+ {
53
+ "label": "Auto-resolutions",
54
+ "page": "auto-resolutions",
55
+ "badge": {
56
+ "source": "app",
57
+ "table": "escalation_auto_resolutions__read_model",
58
+ "filter": [
59
+ {
60
+ "field": "review_state",
61
+ "eq": "pending"
62
+ }
63
+ ],
64
+ "tone": "warn",
65
+ "refreshMs": 5000,
66
+ "hideWhenZero": true
67
+ }
68
+ },
52
69
  {
53
70
  "label": "Cockpit",
54
71
  "page": "cockpit"
@@ -0,0 +1,162 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:bpmndi_1="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:zeebe="http://camunda.org/schema/zeebe/1.0" xmlns:nano="https://nanobpm.io/schema/shapes/1.0" id="Definitions_nano_workforce_escalation_steward" targetNamespace="http://nanobpm.io/nano-workforce">
3
+ <bpmn:process id="escalation-steward" name="Escalation steward" isExecutable="true">
4
+ <bpmn:extensionElements>
5
+ <nano:shapes>
6
+ <nano:shape id="StewardCollectIn" name="Collect eligible escalations — input">
7
+ <nano:extend name="minAgeMs" type="integer" optional="true" />
8
+ </nano:shape>
9
+ <nano:shape id="StewardCollectOut" name="Collect eligible escalations — output">
10
+ <nano:extend name="eligibleCount" type="integer" />
11
+ <nano:extend name="claimed" type="string" list="true" optional="true" />
12
+ <nano:extend name="skippedOverlap" type="boolean" optional="true" />
13
+ </nano:shape>
14
+ <nano:shape id="StewardRecordIn" name="Record steward resolutions — input">
15
+ <nano:extend name="resolutions" type="string" optional="true" />
16
+ </nano:shape>
17
+ </nano:shapes>
18
+ </bpmn:extensionElements>
19
+ <bpmn:startEvent id="tick" name="Steward tick">
20
+ <bpmn:outgoing>es_toCollect</bpmn:outgoing>
21
+ </bpmn:startEvent>
22
+ <bpmn:serviceTask id="collect" name="Collect eligible escalations">
23
+ <bpmn:extensionElements>
24
+ <zeebe:taskDefinition type="pr.escalation-steward-collect" />
25
+ <zeebe:properties>
26
+ <zeebe:property name="io.nanobpm.dataEnvelope.in" value="StewardCollectIn" />
27
+ <zeebe:property name="io.nanobpm.dataEnvelope.out" value="StewardCollectOut" />
28
+ </zeebe:properties>
29
+ </bpmn:extensionElements>
30
+ <bpmn:incoming>es_toCollect</bpmn:incoming>
31
+ <bpmn:outgoing>es_collected</bpmn:outgoing>
32
+ </bpmn:serviceTask>
33
+ <bpmn:exclusiveGateway id="gw_any" name="any eligible?" default="es_done">
34
+ <bpmn:incoming>es_collected</bpmn:incoming>
35
+ <bpmn:outgoing>es_dispatch</bpmn:outgoing>
36
+ <bpmn:outgoing>es_done</bpmn:outgoing>
37
+ </bpmn:exclusiveGateway>
38
+ <bpmn:serviceTask id="steward-agent" name="Review escalations (agent)">
39
+ <bpmn:extensionElements>
40
+ <zeebe:taskDefinition type="senior:escalation-steward" />
41
+ <zeebe:agentDefinition agentType="external" />
42
+ <zeebe:linkedResources>
43
+ <zeebe:linkedResource resourceId="prompts/escalation-steward.md" bindingType="latest" resourceType="GenericScript" linkName="prompt" />
44
+ </zeebe:linkedResources>
45
+ <zeebe:ioMapping>
46
+ <zeebe:input source="=claimed" target="claimedTasks" />
47
+ <zeebe:output source="=if (is defined(resolutions)) then resolutions else null" target="resolutions" />
48
+ </zeebe:ioMapping>
49
+ </bpmn:extensionElements>
50
+ <bpmn:incoming>es_dispatch</bpmn:incoming>
51
+ <bpmn:outgoing>es_toRecord</bpmn:outgoing>
52
+ </bpmn:serviceTask>
53
+ <bpmn:boundaryEvent id="be_steward_sla" name="Agent SLA elapsed" attachedToRef="steward-agent">
54
+ <bpmn:outgoing>es_sla</bpmn:outgoing>
55
+ <bpmn:timerEventDefinition id="ted_steward_sla">
56
+ <bpmn:timeDuration xsi:type="bpmn:tFormalExpression">=agentSlaTimeout</bpmn:timeDuration>
57
+ </bpmn:timerEventDefinition>
58
+ </bpmn:boundaryEvent>
59
+ <bpmn:serviceTask id="record" name="Record steward resolutions">
60
+ <bpmn:extensionElements>
61
+ <zeebe:taskDefinition type="pr.escalation-steward-record" />
62
+ <zeebe:properties>
63
+ <zeebe:property name="io.nanobpm.dataEnvelope.in" value="StewardRecordIn" />
64
+ </zeebe:properties>
65
+ </bpmn:extensionElements>
66
+ <bpmn:incoming>es_toRecord</bpmn:incoming>
67
+ <bpmn:outgoing>es_recorded</bpmn:outgoing>
68
+ </bpmn:serviceTask>
69
+ <bpmn:endEvent id="done" name="Tick complete">
70
+ <bpmn:incoming>es_done</bpmn:incoming>
71
+ <bpmn:incoming>es_recorded</bpmn:incoming>
72
+ <bpmn:incoming>es_sla</bpmn:incoming>
73
+ </bpmn:endEvent>
74
+ <bpmn:sequenceFlow id="es_toCollect" sourceRef="tick" targetRef="collect" />
75
+ <bpmn:sequenceFlow id="es_collected" sourceRef="collect" targetRef="gw_any" />
76
+ <bpmn:sequenceFlow id="es_dispatch" name="eligible" sourceRef="gw_any" targetRef="steward-agent">
77
+ <bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">=eligibleCount &gt; 0</bpmn:conditionExpression>
78
+ </bpmn:sequenceFlow>
79
+ <bpmn:sequenceFlow id="es_done" name="none" sourceRef="gw_any" targetRef="done" />
80
+ <bpmn:sequenceFlow id="es_toRecord" sourceRef="steward-agent" targetRef="record" />
81
+ <bpmn:sequenceFlow id="es_recorded" sourceRef="record" targetRef="done" />
82
+ <bpmn:sequenceFlow id="es_sla" name="agent SLA elapsed" sourceRef="be_steward_sla" targetRef="done" />
83
+ </bpmn:process>
84
+ <bpmndi_1:BPMNDiagram id="BPMNDiagram_escalation-steward">
85
+ <bpmndi_1:BPMNPlane id="BPMNPlane_escalation-steward" bpmnElement="escalation-steward">
86
+ <bpmndi_1:BPMNShape id="BPMNShape_tick" bpmnElement="tick">
87
+ <dc:Bounds x="80" y="102" width="36" height="36" />
88
+ <bpmndi_1:BPMNLabel>
89
+ <dc:Bounds x="53" y="143" width="90" height="14" />
90
+ </bpmndi_1:BPMNLabel>
91
+ </bpmndi_1:BPMNShape>
92
+ <bpmndi_1:BPMNShape id="BPMNShape_collect" bpmnElement="collect">
93
+ <dc:Bounds x="216" y="80" width="100" height="80" />
94
+ </bpmndi_1:BPMNShape>
95
+ <bpmndi_1:BPMNShape id="BPMNShape_gw_any" bpmnElement="gw_any" isMarkerVisible="true">
96
+ <dc:Bounds x="416" y="95" width="50" height="50" />
97
+ <bpmndi_1:BPMNLabel>
98
+ <dc:Bounds x="408" y="62" width="67" height="28" />
99
+ </bpmndi_1:BPMNLabel>
100
+ </bpmndi_1:BPMNShape>
101
+ <bpmndi_1:BPMNShape id="BPMNShape_steward-agent" bpmnElement="steward-agent">
102
+ <dc:Bounds x="566" y="240" width="100" height="80" />
103
+ </bpmndi_1:BPMNShape>
104
+ <bpmndi_1:BPMNShape id="BPMNShape_record" bpmnElement="record">
105
+ <dc:Bounds x="766" y="240" width="100" height="80" />
106
+ </bpmndi_1:BPMNShape>
107
+ <bpmndi_1:BPMNShape id="BPMNShape_done" bpmnElement="done">
108
+ <dc:Bounds x="966" y="102" width="36" height="36" />
109
+ <bpmndi_1:BPMNLabel>
110
+ <dc:Bounds x="953" y="69" width="63" height="28" />
111
+ </bpmndi_1:BPMNLabel>
112
+ </bpmndi_1:BPMNShape>
113
+ <bpmndi_1:BPMNShape id="BPMNShape_be_steward_sla" bpmnElement="be_steward_sla">
114
+ <dc:Bounds x="598" y="302" width="36" height="36" />
115
+ <bpmndi_1:BPMNLabel>
116
+ <dc:Bounds x="580" y="363" width="72" height="28" />
117
+ </bpmndi_1:BPMNLabel>
118
+ </bpmndi_1:BPMNShape>
119
+ <bpmndi_1:BPMNEdge id="BPMNEdge_es_toCollect" bpmnElement="es_toCollect">
120
+ <di:waypoint x="116" y="120" />
121
+ <di:waypoint x="216" y="120" />
122
+ </bpmndi_1:BPMNEdge>
123
+ <bpmndi_1:BPMNEdge id="BPMNEdge_es_collected" bpmnElement="es_collected">
124
+ <di:waypoint x="316" y="120" />
125
+ <di:waypoint x="416" y="120" />
126
+ </bpmndi_1:BPMNEdge>
127
+ <bpmndi_1:BPMNEdge id="BPMNEdge_es_done" bpmnElement="es_done">
128
+ <di:waypoint x="466" y="120" />
129
+ <di:waypoint x="966" y="120" />
130
+ <bpmndi_1:BPMNLabel>
131
+ <dc:Bounds x="700" y="98" width="32" height="14" />
132
+ </bpmndi_1:BPMNLabel>
133
+ </bpmndi_1:BPMNEdge>
134
+ <bpmndi_1:BPMNEdge id="BPMNEdge_es_dispatch" bpmnElement="es_dispatch">
135
+ <di:waypoint x="441" y="145" />
136
+ <di:waypoint x="441" y="280" />
137
+ <di:waypoint x="566" y="280" />
138
+ <bpmndi_1:BPMNLabel>
139
+ <dc:Bounds x="446" y="206" width="60" height="14" />
140
+ </bpmndi_1:BPMNLabel>
141
+ </bpmndi_1:BPMNEdge>
142
+ <bpmndi_1:BPMNEdge id="BPMNEdge_es_toRecord" bpmnElement="es_toRecord">
143
+ <di:waypoint x="666" y="280" />
144
+ <di:waypoint x="766" y="280" />
145
+ </bpmndi_1:BPMNEdge>
146
+ <bpmndi_1:BPMNEdge id="BPMNEdge_es_recorded" bpmnElement="es_recorded">
147
+ <di:waypoint x="866" y="280" />
148
+ <di:waypoint x="984" y="280" />
149
+ <di:waypoint x="984" y="138" />
150
+ </bpmndi_1:BPMNEdge>
151
+ <bpmndi_1:BPMNEdge id="BPMNEdge_es_sla" bpmnElement="es_sla">
152
+ <di:waypoint x="616" y="338" />
153
+ <di:waypoint x="616" y="358" />
154
+ <di:waypoint x="984" y="358" />
155
+ <di:waypoint x="984" y="138" />
156
+ <bpmndi_1:BPMNLabel>
157
+ <dc:Bounds x="765" y="325" width="70" height="28" />
158
+ </bpmndi_1:BPMNLabel>
159
+ </bpmndi_1:BPMNEdge>
160
+ </bpmndi_1:BPMNPlane>
161
+ </bpmndi_1:BPMNDiagram>
162
+ </bpmn:definitions>
@@ -0,0 +1,87 @@
1
+ # Escalation steward — review open escalations and recommend a choice for each
2
+
3
+ You are the **escalation steward** for an opt-in fleet. Once per tick you are handed a
4
+ set of **claimed** open escalations — each a human-in-the-loop wait that is parking a
5
+ piece of work — and your job is to investigate each one and either **resolve** it
6
+ (submit the recommended choice) or **abstain** (deliberately leave it for a human).
7
+
8
+ Every decision you make is written to a durable, reviewable record and shown to an
9
+ operator in the **Auto-resolutions** view, so a human can examine or step in after the
10
+ fact. You are an agent acting with **agent** attribution — your answers are never
11
+ recorded as a human decision.
12
+
13
+ ## Job input (`job.variables`)
14
+
15
+ | var | meaning |
16
+ |----------------|-------------------------------------------------------------------------|
17
+ | `claimedTasks` | an array of JSON strings, one per claimed escalation. Parse each. |
18
+
19
+ Each parsed entry carries:
20
+
21
+ | field | meaning |
22
+ |-----------------------|----------------------------------------------------------------|
23
+ | `user_task_key` | the completable user-task key you act on |
24
+ | `element_id` | the BPMN element id (the escalation kind) |
25
+ | `escalation_kind` | the human-readable kind label |
26
+ | `subject_type` | `feature` \| `plan` \| `pr` \| `delivery` |
27
+ | `subject_key` | the subject's key (e.g. `owner/repo#123`, a run key) |
28
+ | `subject_url` | an external link (the PR/issue URL), when known |
29
+ | `process_instance_key`| the owning engine process instance |
30
+ | `question` | the escalation question / findings, when recorded |
31
+
32
+ ## Evidence before choosing — verify, don't guess
33
+
34
+ For **every** escalation, before you choose, run the same checks an operator would:
35
+
36
+ - Read the escalation's **question, options and form schema**. Use the form's real keys
37
+ (for example `decision` / `value` / `escalationNote` for delivery tasks, `answer` for
38
+ a `wait-answer`, `resolution` / `answer` for a feature escalation).
39
+ - **Verify any claim that matters**, using the repo and the engine:
40
+ - Does the PR, branch or WIP ref actually exist? (`gh pr view`, `git ls-remote`)
41
+ - Were commits pushed? What do the review threads say?
42
+ - Was this question ruled on before? Check `pr_adjudications` (via the
43
+ `getPrHistory` / prior-resolution context) and earlier auto-resolutions.
44
+ - Choose an option and, where the option allows, write a **corrective note** (for
45
+ example a `Retry` with guidance on what to fix).
46
+
47
+ ## When you MUST abstain
48
+
49
+ Abstain (record `abstained`, leave the escalation for a human) when **any** of these
50
+ hold:
51
+
52
+ - The right choice is **unclear** after investigation.
53
+ - The option is **irreversible or destructive** — for example *abandon*, deleting work,
54
+ or merging past a failing required check.
55
+ - A **credential or permission** change is needed.
56
+ - The **loop guard** trips: the same subject has already been auto-resolved twice and
57
+ has escalated again. Repeated escalation means the automatic answer isn't working —
58
+ say so in your rationale.
59
+
60
+ When in doubt, abstain. A human can always answer; a wrong automatic answer is worse.
61
+
62
+ ## Output contract
63
+
64
+ Write a JSON object to the file named by the `AGENT_RESULT_FILE` environment variable
65
+ (or, as a fallback, print a single last stdout line `::nano:result:: {json}`). The
66
+ object MUST carry a `resolutions` field: a JSON **string** encoding an array with one
67
+ entry per claimed escalation:
68
+
69
+ ```json
70
+ {
71
+ "resolutions": "[{\"userTaskKey\":\"...\",\"outcome\":\"resolved\",\"choice\":\"retry\",\"variables\":{\"answer\":\"...\"},\"rationale\":\"...\",\"evidence\":\"...\",\"options\":[\"retry\",\"abandon\"]}]"
72
+ }
73
+ ```
74
+
75
+ Each entry:
76
+
77
+ - `userTaskKey` (required) — the escalation this decision is for.
78
+ - `outcome` (required) — `"resolved"` or `"abstained"`.
79
+ - `variables` (required when `resolved`) — the exact typed form variables to submit.
80
+ - `choice` — the option value you picked (used by the host's destructive-option guard).
81
+ - `rationale` — one line: why this choice (or why you abstained).
82
+ - `evidence` — what you checked, with links (a short markdown/JSON note).
83
+ - `options` — the option values the form offered (a snapshot for the record).
84
+
85
+ **Emitting a machine-readable result is your mandatory final step — never exit
86
+ silently.** Exit `0` on every path. If you cannot reach a confident answer for an
87
+ escalation, record it as `abstained` with the reason — do not guess.