@nanobpm/nano-workforce 0.169.0 → 0.170.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 +12 -0
- package/app/agentGuide.ts +1 -0
- package/app/mcpExclusions.test.ts +1 -0
- package/app/prParse.ts +34 -0
- package/app/service.ts +31 -47
- package/app/userTasks.test.ts +36 -0
- package/app/userTasks.ts +75 -0
- package/docs/agent-guide.md +198 -42
- package/docs/mcp-runbook.md +21 -4
- package/e2e/convergence-escalation.e2e.ts +19 -7
- package/e2e/retire-escalation-subsystem.e2e.ts +18 -5
- package/openapi.yaml +134 -2
- package/operations/listActivePrs.test.ts +31 -13
- package/operations/listEscalations.test.ts +215 -0
- package/operations/listEscalations.ts +32 -0
- package/package.json +1 -1
- package/pages/overview.page.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
## [0.170.1](https://github.com/nanobpm/nano-workforce/compare/v0.170.0...v0.170.1) (2026-08-31)
|
|
2
|
+
|
|
3
|
+
### Documentation
|
|
4
|
+
|
|
5
|
+
* **agent-guide:** make the operator guide tool-aware + tool↔curl crosswalk (N1) ([#669](https://github.com/nanobpm/nano-workforce/issues/669)) ([44c5937](https://github.com/nanobpm/nano-workforce/commit/44c5937fb3a1cdaa5a106d67ed16225c35d614f6)), closes [#664](https://github.com/nanobpm/nano-workforce/issues/664) [#666](https://github.com/nanobpm/nano-workforce/issues/666) [#667](https://github.com/nanobpm/nano-workforce/issues/667) [#665](https://github.com/nanobpm/nano-workforce/issues/665) [666/#667](https://github.com/666/nano-workforce/issues/667) [#666](https://github.com/nanobpm/nano-workforce/issues/666) [#667](https://github.com/nanobpm/nano-workforce/issues/667) [#665](https://github.com/nanobpm/nano-workforce/issues/665)
|
|
6
|
+
|
|
7
|
+
## [0.170.0](https://github.com/nanobpm/nano-workforce/compare/v0.169.0...v0.170.0) (2026-08-31)
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* **api:** add listEscalations read tool + structured openEscalation ([#672](https://github.com/nanobpm/nano-workforce/issues/672)) ([893d39a](https://github.com/nanobpm/nano-workforce/commit/893d39a644cd18102422186b4baddc6779b3510d)), closes [#666](https://github.com/nanobpm/nano-workforce/issues/666) [#666](https://github.com/nanobpm/nano-workforce/issues/666) [#358](https://github.com/nanobpm/nano-workforce/issues/358) [owner/repo#N](https://github.com/owner/repo/issues/N)
|
|
12
|
+
|
|
1
13
|
## [0.169.0](https://github.com/nanobpm/nano-workforce/compare/v0.168.2...v0.169.0) (2026-08-31)
|
|
2
14
|
|
|
3
15
|
### Features
|
package/app/agentGuide.ts
CHANGED
|
@@ -106,6 +106,7 @@ export const GUIDE_SECTIONS: readonly GuideSectionMeta[] = [
|
|
|
106
106
|
{ id: "unstick", summary: "Unstick a wedged process — publish a correlating message, cancel, or otherwise recover a stalled instance." },
|
|
107
107
|
{ id: "raise-issue", summary: "Raise an issue or open a PR against the nano-workforce repository itself." },
|
|
108
108
|
{ id: "delivery-graphs", summary: "Author, preview, compile/stage and run an agent-authored delivery graph (ADR 0005): node/wait/connector vocabulary." },
|
|
109
|
+
{ id: "tool-crosswalk", summary: "Tool↔curl crosswalk: map every guide action to its projected MCP tool (status, version, urban_debug_* engine reads, escalation answer, cancel) with the curl no-MCP fallback." },
|
|
109
110
|
] as const;
|
|
110
111
|
|
|
111
112
|
/** A parsed section: its stable id + summary (from the registry), the derived heading `title` (the
|
package/app/prParse.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Canonical PR-key parser (extracted from app/service.ts so leaf modules can reuse the ONE
|
|
2
|
+
// implementation of the `owner/repo#N` shape without importing the heavy service module — which
|
|
3
|
+
// imports them, so a back-import would cycle). `app/service.ts` re-exports `parsePr`/`ParsedPr`
|
|
4
|
+
// from here, so every existing `import { parsePr } from "./service.ts"` keeps resolving. This is
|
|
5
|
+
// the single source of truth for "is this string a PR key?" — do not add a second shape regex.
|
|
6
|
+
export interface ParsedPr {
|
|
7
|
+
repo: string;
|
|
8
|
+
number: number;
|
|
9
|
+
url: string;
|
|
10
|
+
prKey: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Parse "owner/repo#123" or a canonical PR URL into its parts, or `null` when the input is not a
|
|
14
|
+
* PR key/URL. */
|
|
15
|
+
export function parsePr(input: unknown): ParsedPr | null {
|
|
16
|
+
// Total on any input: a process-variable regression (or an older in-flight instance) can carry a
|
|
17
|
+
// non-string prKey, and `.trim()` on a non-string throws — turning a should-fail-open caller into
|
|
18
|
+
// a retrying job. Fail closed to `null` here so every caller resolves safely instead of throwing.
|
|
19
|
+
if (typeof input !== "string") return null;
|
|
20
|
+
const s = input.trim();
|
|
21
|
+
let m = s.match(/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)/i);
|
|
22
|
+
if (m) {
|
|
23
|
+
const repo = `${m[1]}/${m[2]}`;
|
|
24
|
+
const number = Number(m[3]);
|
|
25
|
+
return { repo, number, url: `https://github.com/${repo}/pull/${number}`, prKey: `${repo}#${number}` };
|
|
26
|
+
}
|
|
27
|
+
m = s.match(/^([^/]+\/[^#]+)#(\d+)$/);
|
|
28
|
+
if (m) {
|
|
29
|
+
const repo = m[1];
|
|
30
|
+
const number = Number(m[2]);
|
|
31
|
+
return { repo, number, url: `https://github.com/${repo}/pull/${number}`, prKey: `${repo}#${number}` };
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|
package/app/service.ts
CHANGED
|
@@ -75,6 +75,7 @@ import {
|
|
|
75
75
|
planTasks,
|
|
76
76
|
} from "./plan.ts";
|
|
77
77
|
import { derivePromotionState, isEpicIntegrationBranch, isPromotable, promotionPrBody, promotionPrTitle } from "./promotion.ts";
|
|
78
|
+
import { type ParsedPr, parsePr } from "./prParse.ts";
|
|
78
79
|
import {
|
|
79
80
|
defaultProbeExec,
|
|
80
81
|
type ProbeExec,
|
|
@@ -93,12 +94,14 @@ import {
|
|
|
93
94
|
latestOpenEscalationQuestion,
|
|
94
95
|
latestPlanReviewFindings,
|
|
95
96
|
latestTrialMergeQuestion,
|
|
97
|
+
type OpenEscalation,
|
|
96
98
|
PLAN_REVIEW_ELEMENT,
|
|
97
99
|
PR_WAIT_ANSWER_ELEMENT,
|
|
98
100
|
PR_WAIT_MERGE_ANSWER_ELEMENT,
|
|
99
101
|
prEscalations,
|
|
100
102
|
reconcileUserTasks,
|
|
101
103
|
TRIAL_MERGE_ELEMENT,
|
|
104
|
+
toOpenEscalation,
|
|
102
105
|
type UserTaskContext,
|
|
103
106
|
type UserTaskRow,
|
|
104
107
|
userTaskKindLabel,
|
|
@@ -314,13 +317,6 @@ const prsTracking = (data: DataLayer) =>
|
|
|
314
317
|
const escs = (data: DataLayer) => data.table<Escalation>("escalations", "id");
|
|
315
318
|
const deps = (data: DataLayer) => data.table<PrDependency>("pr_dependencies", "pr_key");
|
|
316
319
|
|
|
317
|
-
export interface ParsedPr {
|
|
318
|
-
repo: string;
|
|
319
|
-
number: number;
|
|
320
|
-
url: string;
|
|
321
|
-
prKey: string;
|
|
322
|
-
}
|
|
323
|
-
|
|
324
320
|
/** Canonical GitHub PR URL for a repo + number. Matches the `url` `parsePr` derives, so a
|
|
325
321
|
* reconstructed row is indistinguishable from one registered at submit time. */
|
|
326
322
|
export function canonicalPrUrl(repo: string, number: number): string {
|
|
@@ -379,27 +375,10 @@ export async function ensurePr(
|
|
|
379
375
|
}
|
|
380
376
|
}
|
|
381
377
|
|
|
382
|
-
/** Parse "owner/repo#123" or a canonical PR URL into its parts.
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
// a retrying job. Fail closed to `null` here so every caller resolves safely instead of throwing.
|
|
387
|
-
if (typeof input !== "string") return null;
|
|
388
|
-
const s = input.trim();
|
|
389
|
-
let m = s.match(/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)/i);
|
|
390
|
-
if (m) {
|
|
391
|
-
const repo = `${m[1]}/${m[2]}`;
|
|
392
|
-
const number = Number(m[3]);
|
|
393
|
-
return { repo, number, url: `https://github.com/${repo}/pull/${number}`, prKey: `${repo}#${number}` };
|
|
394
|
-
}
|
|
395
|
-
m = s.match(/^([^/]+\/[^#]+)#(\d+)$/);
|
|
396
|
-
if (m) {
|
|
397
|
-
const repo = m[1];
|
|
398
|
-
const number = Number(m[2]);
|
|
399
|
-
return { repo, number, url: `https://github.com/${repo}/pull/${number}`, prKey: `${repo}#${number}` };
|
|
400
|
-
}
|
|
401
|
-
return null;
|
|
402
|
-
}
|
|
378
|
+
/** Parse "owner/repo#123" or a canonical PR URL into its parts. Canonical implementation lives in
|
|
379
|
+
* `./prParse.ts` (a leaf module other leaf modules can reuse without cycling through this one); re-exported
|
|
380
|
+
* here so existing `import { parsePr } from "./service.ts"` call sites keep resolving. */
|
|
381
|
+
export { type ParsedPr, parsePr };
|
|
403
382
|
|
|
404
383
|
/** Extract `Depends-on: owner/repo#N[, owner/repo#N …]` (or PR URLs) from a PR body. Multiple
|
|
405
384
|
* `Depends-on:` lines accumulate; each line may list several comma/space-separated refs. Returns
|
|
@@ -785,7 +764,7 @@ export interface ActivePr {
|
|
|
785
764
|
round: number;
|
|
786
765
|
processKey: string | null;
|
|
787
766
|
waitingSince: string | null;
|
|
788
|
-
openEscalation:
|
|
767
|
+
openEscalation: OpenEscalation | null;
|
|
789
768
|
updatedAt: string;
|
|
790
769
|
/** Leasing worker while an agent is actively working the review round; null when queued
|
|
791
770
|
* (job created, not yet activated) or not at the review-round task. */
|
|
@@ -796,15 +775,15 @@ export interface ActivePr {
|
|
|
796
775
|
|
|
797
776
|
/** Every tracked PR not in a terminal state (converged/abandoned), newest-updated first. Backs
|
|
798
777
|
* the GET status endpoint so an operator or an external harness can see what is in flight
|
|
799
|
-
* without reading the datasource directly. The
|
|
800
|
-
*
|
|
801
|
-
*
|
|
802
|
-
*
|
|
803
|
-
* `
|
|
804
|
-
*
|
|
805
|
-
*
|
|
806
|
-
* BOTH loops' escalations uniformly. Once
|
|
807
|
-
* derives back to null. */
|
|
778
|
+
* without reading the datasource directly. The structured `openEscalation` pointer (issue #666:
|
|
779
|
+
* `{ userTaskKey, kind, summary }`) is derived from the canonical `user_tasks` read model — the SAME
|
|
780
|
+
* user-task surface `listEscalations` and the Convergence/Tasks page consume, so `/status` carries the
|
|
781
|
+
* completable `userTaskKey` without a denormalised PR-row pointer or a second source of truth. A
|
|
782
|
+
* `user_tasks` row for a PR subject exists iff its review/merge-loop escalation task is currently OPEN
|
|
783
|
+
* (parked awaiting a human answer) — the same live-escalation signal a `status="escalated"` PR carries.
|
|
784
|
+
* Both loops park on a native user task answered through the one canonical `completeUserTask` door
|
|
785
|
+
* (#256), so deriving from the read model surfaces BOTH loops' escalations uniformly. Once the task is
|
|
786
|
+
* completed its row is removed, so `openEscalation` derives back to null. */
|
|
808
787
|
export async function activePrs(data: DataLayer): Promise<ActivePr[]> {
|
|
809
788
|
const all = await prsTracking(data).all();
|
|
810
789
|
const active = all
|
|
@@ -818,15 +797,20 @@ export async function activePrs(data: DataLayer): Promise<ActivePr[]> {
|
|
|
818
797
|
// `derived_status === status`.
|
|
819
798
|
.filter((p) => !TERMINAL_STATUSES.includes(p.derived_status))
|
|
820
799
|
.sort((a, b) => (a.updated_at < b.updated_at ? 1 : a.updated_at > b.updated_at ? -1 : 0));
|
|
821
|
-
//
|
|
822
|
-
//
|
|
823
|
-
//
|
|
824
|
-
//
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
800
|
+
// Derive the STRUCTURED open-escalation pointer from the ONE `user_tasks` read model (issue #666) —
|
|
801
|
+
// the same user-task surface `listEscalations` and the Convergence/Tasks page consume — so `/status`
|
|
802
|
+
// carries the completable `userTaskKey` (plus the escalation `kind` and a one-line `summary`) without
|
|
803
|
+
// a second source of truth. A `user_tasks` row for a PR subject exists iff its review/merge-loop
|
|
804
|
+
// escalation task is currently OPEN (parked awaiting a human answer), so its presence is exactly the
|
|
805
|
+
// live-escalation signal the old `escalations`-table derivation computed — now unified across BOTH
|
|
806
|
+
// loops. Keep the newest per PR (a PR parks on at most one such task at a time; order by recency for
|
|
807
|
+
// determinism).
|
|
808
|
+
const openEscByPr = new Map<string, OpenEscalation>();
|
|
809
|
+
for (const t of (await userTasks(data).find({ subject_type: "pr" })).sort((a, b) =>
|
|
810
|
+
a.updated_at < b.updated_at ? 1 : a.updated_at > b.updated_at ? -1 : 0,
|
|
811
|
+
)) {
|
|
812
|
+
if (openEscByPr.has(t.subject_key)) continue;
|
|
813
|
+
openEscByPr.set(t.subject_key, toOpenEscalation(t));
|
|
830
814
|
}
|
|
831
815
|
return active.map((p) => ({
|
|
832
816
|
prKey: p.pr_key,
|
package/app/userTasks.test.ts
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
latestTrialMergeQuestion,
|
|
20
20
|
type PrEscalationRow,
|
|
21
21
|
reconcileUserTasks,
|
|
22
|
+
toEscalationView,
|
|
22
23
|
TRIAL_MERGE_ELEMENT,
|
|
23
24
|
userTaskKindLabel,
|
|
24
25
|
type UserTaskRow,
|
|
@@ -337,3 +338,38 @@ test("latestFeatureEscalationQuestion: picks the newest audit row (highest id),
|
|
|
337
338
|
test("latestFeatureEscalationQuestion: null when the feature has no recorded escalation", () => {
|
|
338
339
|
assertEquals(latestFeatureEscalationQuestion([]), null);
|
|
339
340
|
});
|
|
341
|
+
|
|
342
|
+
// toEscalationView.prKey must be a genuine PR key (owner/repo#N), never a subject key that fell back
|
|
343
|
+
// to a non-PR value. `buildUserTaskRow` coalesces a blank subjectKey to `processKey`/`userTaskKey`
|
|
344
|
+
// for an orphaned/untracked instance (#358); for a PR-subject task that yields `subject_key` = a
|
|
345
|
+
// numeric engine key, which must NOT be emitted as `prKey` (OpenAPI: prKey is the PR key only when
|
|
346
|
+
// known). Copilot review suppressed advisory app/userTasks.ts:136.
|
|
347
|
+
const escRow = (over: Partial<UserTaskRow>): UserTaskRow => ({
|
|
348
|
+
user_task_key: "ut-1",
|
|
349
|
+
element_id: PR_WAIT_ANSWER_ELEMENT,
|
|
350
|
+
kind_label: "PR review",
|
|
351
|
+
subject_type: "pr",
|
|
352
|
+
subject_key: "o/r#7",
|
|
353
|
+
subject_title: "o/r#7",
|
|
354
|
+
subject_url: null,
|
|
355
|
+
question: null,
|
|
356
|
+
process_key: null,
|
|
357
|
+
form_key: null,
|
|
358
|
+
created_at: AT,
|
|
359
|
+
updated_at: AT,
|
|
360
|
+
...over,
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
test("toEscalationView: PR-subject row with a PR-shaped subject key emits it as prKey", () => {
|
|
364
|
+
assertEquals(toEscalationView(escRow({ subject_key: "o/r#7" })).prKey, "o/r#7");
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
test("toEscalationView: PR-subject row whose subject key fell back to a non-PR value yields prKey null (still correlatable via subjectKey)", () => {
|
|
368
|
+
const view = toEscalationView(escRow({ subject_key: "19153" }));
|
|
369
|
+
assertEquals(view.prKey, null);
|
|
370
|
+
assertEquals(view.subjectKey, "19153");
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
test("toEscalationView: non-PR subject always yields prKey null", () => {
|
|
374
|
+
assertEquals(toEscalationView(escRow({ subject_type: "plan", subject_key: "o/r#7" })).prKey, null);
|
|
375
|
+
});
|
package/app/userTasks.ts
CHANGED
|
@@ -22,6 +22,7 @@ import { CONFORMANCE_ESCALATION_ELEMENT } from "./conformance.ts";
|
|
|
22
22
|
import { DELIVERY_HUMAN_ELEMENT, isDeliveryHumanElement } from "./deliveryHuman.ts";
|
|
23
23
|
import { FEATURE_BLOCKED_ELEMENT, FEATURE_ESCALATION_ELEMENT, type FeatureEscalationRow } from "./feature.ts";
|
|
24
24
|
import type { PlanReview } from "./plan.ts";
|
|
25
|
+
import { parsePr } from "./prParse.ts";
|
|
25
26
|
import type { TrialMergeAuditRow } from "./trialMerge.ts";
|
|
26
27
|
|
|
27
28
|
const now = () => new Date().toISOString();
|
|
@@ -98,6 +99,80 @@ export interface UserTaskRow {
|
|
|
98
99
|
|
|
99
100
|
export const userTasks = (data: DataLayer) => data.table<UserTaskRow>("user_tasks", "user_task_key");
|
|
100
101
|
|
|
102
|
+
/** The read-tool projection of ONE open escalation user task (issue #666, epic #664 — retire the
|
|
103
|
+
* `/tasks/api/tasks` inbox curl). Sourced from the SAME `user_tasks` read model the Tasks inbox and
|
|
104
|
+
* Convergence page consume — NOT a second source of truth. It carries the completable `userTaskKey`
|
|
105
|
+
* an agent answers via `completeUserTask` / `agentCompleteEscalation`, the escalation `kind` (the
|
|
106
|
+
* BPMN `elementId`), the denormalised decision text (`question` — the question / findings / task the
|
|
107
|
+
* loop or agent raised, uniform across all four kinds), and the deployed-form context so a tool-aware
|
|
108
|
+
* agent can discover and answer an escalation without curling the un-projected task inbox. */
|
|
109
|
+
export interface EscalationView {
|
|
110
|
+
userTaskKey: string;
|
|
111
|
+
kind: string;
|
|
112
|
+
kindLabel: string;
|
|
113
|
+
/** The PR key when this escalation belongs to a PR (review/merge loop): the subject key, but ONLY
|
|
114
|
+
* when it is actually PR-key-shaped (`owner/repo#N`). Null for feature / plan / delivery / agent
|
|
115
|
+
* subjects — and also null for a PR-subject task whose subject key fell back to a non-PR value
|
|
116
|
+
* (`processKey`/`userTaskKey`) for an orphaned/untracked instance (see `buildUserTaskRow`), so
|
|
117
|
+
* `prKey` never emits a non-PR key. Use `subjectKey` for raw correlation in that case. */
|
|
118
|
+
prKey: string | null;
|
|
119
|
+
subjectType: string;
|
|
120
|
+
subjectKey: string;
|
|
121
|
+
subjectTitle: string;
|
|
122
|
+
subjectUrl: string | null;
|
|
123
|
+
question: string | null;
|
|
124
|
+
formKey: string | null;
|
|
125
|
+
processKey: string | null;
|
|
126
|
+
/** The denormalised decision/form context the Tasks inbox renders for this task (the same
|
|
127
|
+
* `question` + subject the deployed `.form` is seeded with). The typed answer fields an agent
|
|
128
|
+
* submits depend on `kind` (e.g. a PR `{ answer }`, a plan-review `{ directive, notes }`). */
|
|
129
|
+
formVariables: Record<string, unknown>;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Pure: project one open `user_tasks` row into its `listEscalations` read-tool entry. Reuses the
|
|
133
|
+
* read model verbatim (no new query / source of truth). `prKey` is the subject key only when the
|
|
134
|
+
* subject is a PR AND the subject key is genuinely PR-key-shaped (`owner/repo#N`, validated by the
|
|
135
|
+
* canonical `parsePr`) — an orphaned PR-loop instance whose subject key fell back to a numeric
|
|
136
|
+
* `processKey`/`userTaskKey` (see `buildUserTaskRow`) yields `prKey: null`, not a non-PR key that
|
|
137
|
+
* would contradict the OpenAPI contract. `subjectKey` still carries the raw value for correlation. */
|
|
138
|
+
export function toEscalationView(row: UserTaskRow): EscalationView {
|
|
139
|
+
return {
|
|
140
|
+
userTaskKey: row.user_task_key,
|
|
141
|
+
kind: row.element_id,
|
|
142
|
+
kindLabel: row.kind_label,
|
|
143
|
+
prKey: row.subject_type === "pr" && parsePr(row.subject_key) ? row.subject_key : null,
|
|
144
|
+
subjectType: row.subject_type,
|
|
145
|
+
subjectKey: row.subject_key,
|
|
146
|
+
subjectTitle: row.subject_title,
|
|
147
|
+
subjectUrl: row.subject_url,
|
|
148
|
+
question: row.question,
|
|
149
|
+
formKey: row.form_key,
|
|
150
|
+
processKey: row.process_key,
|
|
151
|
+
formVariables: {
|
|
152
|
+
question: row.question,
|
|
153
|
+
subjectTitle: row.subject_title,
|
|
154
|
+
subjectUrl: row.subject_url,
|
|
155
|
+
formKey: row.form_key,
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** The structured open-escalation pointer surfaced on each active PR by `/status` (issue #666): the
|
|
161
|
+
* completable `userTaskKey`, the escalation `kind` (BPMN `elementId`), and a one-line `summary` (the
|
|
162
|
+
* raised question / findings). Derived from the SAME `user_tasks` read model as `listEscalations`
|
|
163
|
+
* (no denormalised PR-row pointer, no second source of truth); the field is null when the PR is not
|
|
164
|
+
* parked on an open escalation. */
|
|
165
|
+
export interface OpenEscalation {
|
|
166
|
+
userTaskKey: string;
|
|
167
|
+
kind: string;
|
|
168
|
+
summary: string | null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Pure: the structured `/status` open-escalation pointer for one open PR user-task row. */
|
|
172
|
+
export function toOpenEscalation(row: UserTaskRow): OpenEscalation {
|
|
173
|
+
return { userTaskKey: row.user_task_key, kind: row.element_id, summary: row.question };
|
|
174
|
+
}
|
|
175
|
+
|
|
101
176
|
/** Human-readable label per escalation element. The set of keys is the closed set of user-task
|
|
102
177
|
* elements the Tasks inbox surfaces — an element absent from here is not an escalation and is
|
|
103
178
|
* ignored by `buildUserTaskRow`, so an arbitrary internal user task can never leak into the inbox. */
|