@nanobpm/nano-workforce 0.64.0 → 0.65.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.
@@ -0,0 +1,42 @@
1
+ // pr.record-feature-escalation — a feature run has parked on the native `feature-escalation`
2
+ // user task (the agent reported `status:"escalated"` with a non-blank `question`). This service
3
+ // task runs on the `escalated` arm, immediately BEFORE the user task is created, and persists the
4
+ // escalation onto the `feature_runs` row so the nwf UI can surface it (issue #210):
5
+ // • flips `status` to the non-terminal `escalated` so status-based views and counts flag it, and
6
+ // • denormalises the agent's `question` for the Escalation column + the answer affordance.
7
+ //
8
+ // It deliberately does NOT record a REAL completable `userTaskKey` — the task does not exist yet at
9
+ // this point — and clears any stale pointer so the row reads "key unknown until observed".
10
+ // `pollFeatureEscalations` (app/service.ts) fills that pointer in once the user task is
11
+ // observable via `searchUserTasks`, which is also the reason the question is captured HERE rather
12
+ // than by the poller: the WASM testkit engine does not surface a user task's ioMapping-mapped local
13
+ // variables through `searchUserTasks`, so the process variable must be persisted while it is still
14
+ // in scope on the job.
15
+ import type { AppJobHandler } from "@nanobpm/urban";
16
+ import { featureRuns } from "../../app/feature.ts";
17
+ import type { WorkerInputs } from "../../nano-generated/worker-io.d.ts";
18
+
19
+ // Input typed off the model data envelope (`RecordFeatureEscalationIn` in feature.bpmn) — ADR 0040.
20
+ type In = WorkerInputs["pr.record-feature-escalation"];
21
+
22
+ const nonBlank = (v: unknown): string | null =>
23
+ typeof v === "string" && v.trim().length > 0 ? v.trim() : null;
24
+
25
+ const handler: AppJobHandler<In> = async (job, app) => {
26
+ const featureKey = job.variables.featureKey;
27
+ const question = nonBlank(job.variables.question);
28
+ await featureRuns(app.data).update(featureKey, {
29
+ status: "escalated",
30
+ escalation_question: question,
31
+ // The user task does not exist yet, so any non-null pointer here can only be stale (a prior
32
+ // escalation's key, a manual DB repair). Clear it so the row reads "key unknown until observed"
33
+ // and the pages don't bind the answer/abandon affordance to a dead task; the poller re-fills the
34
+ // real key (it can always re-derive it from `searchUserTasks`), so this clear is never lossy.
35
+ escalation_user_task_key: null,
36
+ updated_at: new Date().toISOString(),
37
+ });
38
+ app.log.info("record-feature-escalation", { featureKey, hasQuestion: question !== null });
39
+ return {};
40
+ };
41
+
42
+ export default handler;