@nanobpm/nano-workforce 0.186.0 → 0.186.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 +6 -0
- package/app/deliveryHuman.test.ts +123 -0
- package/app/deliveryHuman.ts +29 -0
- package/app/pollUserTasks.test.ts +56 -0
- package/app/service.ts +13 -2
- package/package.json +1 -1
- package/resources/forms/delivery-human-generic.form +3 -32
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
## [0.186.1](https://github.com/nanobpm/nano-workforce/compare/v0.186.0...v0.186.1) (2026-09-11)
|
|
2
|
+
|
|
3
|
+
### Bug Fixes
|
|
4
|
+
|
|
5
|
+
* delivery-graph human-node Tasks form renders context-free ([#773](https://github.com/nanobpm/nano-workforce/issues/773)) ([91a8065](https://github.com/nanobpm/nano-workforce/commit/91a806566e157257cc264310e18d7df0bba3e350)), closes [#772](https://github.com/nanobpm/nano-workforce/issues/772) [#772](https://github.com/nanobpm/nano-workforce/issues/772)
|
|
6
|
+
|
|
1
7
|
## [0.186.0](https://github.com/nanobpm/nano-workforce/compare/v0.185.1...v0.186.0) (2026-09-08)
|
|
2
8
|
|
|
3
9
|
### Features
|
|
@@ -10,6 +10,7 @@ import type { DeliveryFact } from "../nano-generated/api-io.d.ts";
|
|
|
10
10
|
import {
|
|
11
11
|
bindHumanEmits,
|
|
12
12
|
DELIVERY_HUMAN_ELEMENT,
|
|
13
|
+
deliveryHumanContextQuestion,
|
|
13
14
|
deriveHumanCategory,
|
|
14
15
|
GENERIC_HUMAN_FORM,
|
|
15
16
|
HUMAN_ACK_FORM,
|
|
@@ -304,3 +305,125 @@ test("drift guard: the human element is completer-answerable and surfaces on the
|
|
|
304
305
|
"USER_TASK_KIND_LABELS must label the delivery human node",
|
|
305
306
|
);
|
|
306
307
|
});
|
|
308
|
+
|
|
309
|
+
// ── issue #772: Tasks-inbox "Decision context" for a parked delivery-graph human node ─────────────
|
|
310
|
+
|
|
311
|
+
test("deliveryHumanContextQuestion: derives the node instruction from human_labels (base + __esc twin)", () => {
|
|
312
|
+
const labels = { "delivery-human-task__n7": "Run the manual OTP publish for @nanobpm/urban" };
|
|
313
|
+
// The parked base task looks up its own id.
|
|
314
|
+
assertEquals(
|
|
315
|
+
deliveryHumanContextQuestion(labels, "delivery-human-task__n7"),
|
|
316
|
+
"Run the manual OTP publish for @nanobpm/urban",
|
|
317
|
+
);
|
|
318
|
+
// The bounded-timeout escalation twin parks on `…__esc`; strip it to find the same stamped label.
|
|
319
|
+
assertEquals(
|
|
320
|
+
deliveryHumanContextQuestion(labels, "delivery-human-task__n7__esc"),
|
|
321
|
+
"Run the manual OTP publish for @nanobpm/urban",
|
|
322
|
+
);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
test("deliveryHumanContextQuestion: falls back to a static message when no label is stored", () => {
|
|
326
|
+
// A parked human step must never render a blank Decision context — an untracked/absent label still
|
|
327
|
+
// yields actionable guidance rather than null (which would leave the panel empty, issue #772).
|
|
328
|
+
assertEquals(
|
|
329
|
+
deliveryHumanContextQuestion({}, "delivery-human-task__n1"),
|
|
330
|
+
"A scheduled delivery-graph step is waiting to be completed.",
|
|
331
|
+
);
|
|
332
|
+
assertEquals(
|
|
333
|
+
deliveryHumanContextQuestion(undefined, "delivery-human-task__n1"),
|
|
334
|
+
"A scheduled delivery-graph step is waiting to be completed.",
|
|
335
|
+
);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
test("deliveryHumanContextQuestion: a non-human node's escalation twin (no stored label) gets the node-NEUTRAL fallback, not a 'human step' claim", () => {
|
|
339
|
+
// `isDeliveryHumanElement` also matches the `__esc`/`__contract` escalation twins that bounded
|
|
340
|
+
// `agent`/`wait`/`connector` nodes schedule; those carry NO stored human label. The fallback must
|
|
341
|
+
// not mislabel them as a "human step" (issue #772 review, comment on service.ts contextFor arm).
|
|
342
|
+
const humanLabels = { "delivery-human-task__n7": "Run the manual OTP publish" };
|
|
343
|
+
assertEquals(
|
|
344
|
+
deliveryHumanContextQuestion(humanLabels, "delivery-human-task__agent5__esc"),
|
|
345
|
+
"A scheduled delivery-graph step is waiting to be completed.",
|
|
346
|
+
);
|
|
347
|
+
assertEquals(
|
|
348
|
+
deliveryHumanContextQuestion(humanLabels, "delivery-human-task__agent5__contract"),
|
|
349
|
+
"A scheduled delivery-graph step is waiting to be completed.",
|
|
350
|
+
);
|
|
351
|
+
// A real human node's own `__esc` timeout twin still resolves the base label.
|
|
352
|
+
assertEquals(
|
|
353
|
+
deliveryHumanContextQuestion(humanLabels, "delivery-human-task__n7__esc"),
|
|
354
|
+
"Run the manual OTP publish",
|
|
355
|
+
);
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
test("deliveryHumanContextQuestion: a human node whose id itself ends in __esc resolves its EXACT label, not a sibling's", () => {
|
|
359
|
+
// Node ids may legitimately end in `__esc` (`deliveryGraph.ts` NODE_ID_PATTERN), so such a node is
|
|
360
|
+
// stamped under the exact key `delivery-human-task__<id>` (…__esc). The exact lookup must win over
|
|
361
|
+
// the `__esc`-stripped base, otherwise a sibling `n7` node's label would shadow real node `n7__esc`.
|
|
362
|
+
const labels = {
|
|
363
|
+
"delivery-human-task__n7": "Sibling n7 label",
|
|
364
|
+
"delivery-human-task__n7__esc": "The real n7__esc node label",
|
|
365
|
+
};
|
|
366
|
+
assertEquals(
|
|
367
|
+
deliveryHumanContextQuestion(labels, "delivery-human-task__n7__esc"),
|
|
368
|
+
"The real n7__esc node label",
|
|
369
|
+
);
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
// ── issue #772: the generic form must be static / input-only on the Tasks surface ─────────────────
|
|
373
|
+
|
|
374
|
+
const genericForm = readFileSync("resources/forms/delivery-human-generic.form", "utf8");
|
|
375
|
+
|
|
376
|
+
test("form-structure guard: delivery-human-generic.form uses node-neutral wording", () => {
|
|
377
|
+
// This shared form is also attached to the `__esc`/`__contract` escalation tasks that bounded
|
|
378
|
+
// agent/wait/connector nodes create (`app/deliveryGraphCompiler.ts`), not only scheduled `human`
|
|
379
|
+
// nodes. Copy that calls the task a "scheduled human step" is inaccurate for the escalation family
|
|
380
|
+
// and can obscure that the task is an escalation, so the static text must stay node-neutral.
|
|
381
|
+
assert(
|
|
382
|
+
!/scheduled human step/i.test(genericForm),
|
|
383
|
+
"the shared generic form must use node-neutral wording (it also serves escalation tasks)",
|
|
384
|
+
);
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
test("form-structure guard: delivery-human-generic.form carries no {{…}} tokens", () => {
|
|
388
|
+
// The Tasks surface (`engineForm`) seeds NO form variables, so any `{{token}}` renders literally and
|
|
389
|
+
// any data-dependent `conditional` mis-fires. Deploy-time `{{token}}` templating is removed too, so
|
|
390
|
+
// such a token could never resolve. Assert the surface stays context-free-safe (issue #772).
|
|
391
|
+
assert(!genericForm.includes("{{"), "the generic delivery-human form must not carry {{…}} tokens");
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
test("form-structure guard: delivery-human-generic.form has no data-dependent conditional and an OPTIONAL value", () => {
|
|
395
|
+
const parsed = JSON.parse(genericForm) as {
|
|
396
|
+
components: { key?: string; conditional?: unknown; validate?: { required?: boolean } }[];
|
|
397
|
+
};
|
|
398
|
+
// No component may gate on form data — those blurbs render contradictorily against empty data.
|
|
399
|
+
for (const c of parsed.components) {
|
|
400
|
+
assert(c.conditional === undefined, "no component may carry a data-dependent conditional");
|
|
401
|
+
}
|
|
402
|
+
// `value` must be OPTIONAL — a no-emit ("click done") node completes without entering a value. This
|
|
403
|
+
// S3 generic surface deploys a single static default form and cannot know per-node whether a fact is
|
|
404
|
+
// required, so it must not client-require `value`; per-node typed-emit binding is the S4 form-
|
|
405
|
+
// selection path's job (`bindHumanEmits`), not a client-required field on this generic surface.
|
|
406
|
+
const value = parsed.components.find((c) => c.key === "value");
|
|
407
|
+
assert(value, "the generic form must keep a single `value` field");
|
|
408
|
+
assert(value!.validate?.required !== true, "`value` must be optional on this surface");
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
test("form-structure guard: delivery-human-generic.form has no task-variable-dependent readonly input", () => {
|
|
412
|
+
// The Tasks surface (`engineForm`) seeds NO task-local variables, so a readonly INPUT component
|
|
413
|
+
// (e.g. the old `prompt` textarea) renders permanently blank — a confusing empty "Now do this".
|
|
414
|
+
// The instruction reaches the operator through the read-model Decision context column instead, so
|
|
415
|
+
// the surface must carry no keyed readonly control; a static keyless `text` block may point to it.
|
|
416
|
+
const parsed = JSON.parse(genericForm) as {
|
|
417
|
+
components: { type?: string; key?: string; readonly?: boolean }[];
|
|
418
|
+
};
|
|
419
|
+
for (const c of parsed.components) {
|
|
420
|
+
assert(
|
|
421
|
+
!(c.readonly === true && typeof c.key === "string"),
|
|
422
|
+
`keyed readonly input '${c.key}' renders blank on the variable-free Tasks surface`,
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
assert(
|
|
426
|
+
!parsed.components.some((c) => c.key === "prompt"),
|
|
427
|
+
"the never-seeded readonly `prompt` control must not reappear on the Tasks surface",
|
|
428
|
+
);
|
|
429
|
+
});
|
package/app/deliveryHuman.ts
CHANGED
|
@@ -54,6 +54,35 @@ export function isDeliveryHumanElement(elementId: string): boolean {
|
|
|
54
54
|
return elementId === DELIVERY_HUMAN_ELEMENT || elementId.startsWith(`${DELIVERY_HUMAN_ELEMENT}__`);
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
/** The read-model "Decision context" for a parked delivery-graph `human` node (issue #772). The Tasks
|
|
58
|
+
* surface renders the deployed `.form` against EMPTY data (it seeds no task-local variables), so the
|
|
59
|
+
* node's instruction cannot reach the operator through the form's readonly `prompt` field — it must
|
|
60
|
+
* arrive through the read-model `question` column, exactly like every other escalation kind. The run
|
|
61
|
+
* row already stamps each human node's instruction label in `human_labels` (`buildHumanLabels`), keyed
|
|
62
|
+
* by the node's base user-task element id (`delivery-human-task__<node>`); the bounded-timeout
|
|
63
|
+
* escalation twin parks on the `…__esc` variant. A node id may itself legitimately END in `__esc`
|
|
64
|
+
* (`deliveryGraph.ts` NODE_ID_PATTERN allows it), so the twin-suffix strip is a FALLBACK, not the
|
|
65
|
+
* first probe: look up the EXACT reported element id first (a real `…__esc` human node stores its
|
|
66
|
+
* label under that exact key), and only then the `__esc`-stripped base (the bounded-timeout twin,
|
|
67
|
+
* whose exact id is never stamped, resolves to its real node's label). Returns a static fallback when
|
|
68
|
+
* no label is stored so a parked step is never left with a blank panel.
|
|
69
|
+
*
|
|
70
|
+
* The fallback is deliberately node-NEUTRAL ("…delivery-graph step…", not "…human step…"): only
|
|
71
|
+
* real `human` nodes are stamped into `human_labels`, but `isDeliveryHumanElement` (and hence this
|
|
72
|
+
* helper's caller) also matches the `__esc`/`__contract` escalation twins that BOUNDED `agent`/`wait`/
|
|
73
|
+
* `connector` nodes schedule — those carry no stored label and would otherwise be mislabeled as a
|
|
74
|
+
* "human step". A found label is always a real human node's instruction; the fallback must read true
|
|
75
|
+
* for both an untracked human run AND a non-human escalation twin. */
|
|
76
|
+
export function deliveryHumanContextQuestion(
|
|
77
|
+
humanLabels: Record<string, string> | undefined,
|
|
78
|
+
elementId: string,
|
|
79
|
+
): string {
|
|
80
|
+
const labels = humanLabels ?? {};
|
|
81
|
+
const base = elementId.replace(/__esc$/, "");
|
|
82
|
+
const label = (labels[elementId] ?? labels[base] ?? "").trim();
|
|
83
|
+
return label || "A scheduled delivery-graph step is waiting to be completed.";
|
|
84
|
+
}
|
|
85
|
+
|
|
57
86
|
/** The GENERIC fallback form (Decision 4, step 3): captures ONE typed value into the node's single
|
|
58
87
|
* declared emitted fact, so a human node with no explicit/category form can STILL emit downstream. */
|
|
59
88
|
export const GENERIC_HUMAN_FORM = "delivery-human-generic";
|
|
@@ -661,6 +661,62 @@ test("pollUserTasks (engine-first): surfaces an inlined delivery-graph human tas
|
|
|
661
661
|
assertEquals(byKey["35002"].subject_title, "release runbook");
|
|
662
662
|
});
|
|
663
663
|
|
|
664
|
+
test("pollUserTasks: a parked delivery-human node carries its instruction as `question` (Decision context, issue #772)", async () => {
|
|
665
|
+
// The Tasks surface seeds no form variables, so a delivery-graph `human` node's instruction can only
|
|
666
|
+
// reach the operator through the read-model `question` (rendered as "Decision context"). Source it
|
|
667
|
+
// from the run's stamped `human_labels`, keyed by the parked user-task element id — else the panel is
|
|
668
|
+
// blank and the human has no idea what the run is waiting on.
|
|
669
|
+
const { data, stores } = memData({
|
|
670
|
+
delivery_graph_runs: [
|
|
671
|
+
{
|
|
672
|
+
run_key: "delivery-graph-403eb22e",
|
|
673
|
+
process_key: "dg-1",
|
|
674
|
+
status: "running",
|
|
675
|
+
title: "release runbook",
|
|
676
|
+
human_labels: JSON.stringify({
|
|
677
|
+
"delivery-human-task__n7": "Run the manual OTP publish for @nanobpm/urban",
|
|
678
|
+
}),
|
|
679
|
+
},
|
|
680
|
+
],
|
|
681
|
+
});
|
|
682
|
+
const restore = stubUserTaskSearch([
|
|
683
|
+
{ userTaskKey: "20411", elementId: "delivery-human-task__n7", processInstanceKey: "dg-1", state: "CREATED" },
|
|
684
|
+
// the bounded-timeout escalation twin parks on the `…__esc` id but resolves the same base label
|
|
685
|
+
{ userTaskKey: "20412", elementId: "delivery-human-task__n7__esc", processInstanceKey: "dg-1", state: "CREATED" },
|
|
686
|
+
]);
|
|
687
|
+
try {
|
|
688
|
+
await pollUserTasks(data, fakeEngine({}), REST);
|
|
689
|
+
} finally {
|
|
690
|
+
restore();
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
const byKey = Object.fromEntries((stores.user_tasks ?? []).map((r) => [r.user_task_key, r]));
|
|
694
|
+
assertEquals(byKey["20411"].question, "Run the manual OTP publish for @nanobpm/urban");
|
|
695
|
+
assertEquals(byKey["20412"].question, "Run the manual OTP publish for @nanobpm/urban");
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
test("pollUserTasks: a delivery-human node with no stored label still gets a non-blank Decision context (issue #772)", async () => {
|
|
699
|
+
// An untracked run (or a run whose label wasn't stamped) must not leave the panel blank — a static,
|
|
700
|
+
// node-NEUTRAL fallback still tells the operator a delivery-graph step is waiting. It must read true
|
|
701
|
+
// for a non-human escalation twin too, so it must not claim "human step".
|
|
702
|
+
const { data, stores } = memData({});
|
|
703
|
+
const restore = stubUserTaskSearch([
|
|
704
|
+
{ userTaskKey: "20411", elementId: "delivery-human-task__n1", processInstanceKey: "dg-9", state: "CREATED" },
|
|
705
|
+
// a bounded `agent`/`wait`/`connector` node's escalation twin: `isDeliveryHumanElement` matches it,
|
|
706
|
+
// but it carries no stored human label, so it gets the neutral fallback, not a "human step" claim.
|
|
707
|
+
{ userTaskKey: "20499", elementId: "delivery-human-task__agent5__esc", processInstanceKey: "dg-9", state: "CREATED" },
|
|
708
|
+
]);
|
|
709
|
+
try {
|
|
710
|
+
await pollUserTasks(data, fakeEngine({}), REST);
|
|
711
|
+
} finally {
|
|
712
|
+
restore();
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
const byKey = Object.fromEntries((stores.user_tasks ?? []).map((r) => [r.user_task_key, r]));
|
|
716
|
+
assertEquals(byKey["20411"].question, "A scheduled delivery-graph step is waiting to be completed.");
|
|
717
|
+
assertEquals(byKey["20499"].question, "A scheduled delivery-graph step is waiting to be completed.");
|
|
718
|
+
});
|
|
719
|
+
|
|
664
720
|
test("pollUserTasks (engine-first): a delivery-human task on an UNTRACKED run still surfaces (bucketed `delivery`, instance fallback) (issue #442)", async () => {
|
|
665
721
|
// Even with no `delivery_graph_runs` row referencing the instance, the kind implies its aggregate, so
|
|
666
722
|
// the row renders and stays answerable — mirroring the orphaned-escalation guarantee (#358).
|
package/app/service.ts
CHANGED
|
@@ -30,7 +30,7 @@ import { isUniqueConstraintFence } from "./dbFence.ts";
|
|
|
30
30
|
import { deriveDelivery, EPIC_LIVE_STATUSES, TERMINAL_STATUSES } from "./delivery.ts";
|
|
31
31
|
import { sweepExpiredProposals } from "./deliveryGraphProposals.ts";
|
|
32
32
|
import { deliveryGraphRuns, deriveDeliveryPhase, parseHumanLabels } from "./deliveryGraphRun.ts";
|
|
33
|
-
import { isDeliveryHumanElement } from "./deliveryHuman.ts";
|
|
33
|
+
import { deliveryHumanContextQuestion, isDeliveryHumanElement } from "./deliveryHuman.ts";
|
|
34
34
|
import { fleetSupportsDurableResume } from "./durableResume.ts";
|
|
35
35
|
import { deriveEpicPhaseLive, deriveTerminalEpicPhase } from "./epicPhase.ts";
|
|
36
36
|
import { deriveFeatureDelivery, FEATURE_BLOCKED_ELEMENT, FEATURE_ESCALATION_ELEMENT, FEATURE_RUN_STATUSES, type FeatureRunStatus, featureEscalations, featureRuns } from "./feature.ts";
|
|
@@ -2613,6 +2613,10 @@ export async function pollUserTasks(
|
|
|
2613
2613
|
url?: string | null;
|
|
2614
2614
|
deliveryLabel?: string | null;
|
|
2615
2615
|
conformanceSummary?: string | null;
|
|
2616
|
+
/** The parked human-node instruction labels for a delivery run, keyed by user-task element id
|
|
2617
|
+
* (`delivery-human-task__<node>`) — the run row's stamped `human_labels`, denormalised so the
|
|
2618
|
+
* Tasks-inbox "Decision context" can explain a parked delivery-graph `human` node (issue #772). */
|
|
2619
|
+
humanLabels?: Record<string, string>;
|
|
2616
2620
|
/** The subject's at-a-glance "waiting on <capability> · …" rollup, denormalised for the readiness
|
|
2617
2621
|
* escalation question (issue #674): the wait-gate projection on `plans.wait_gate_label`, or the
|
|
2618
2622
|
* feature run's `delivery_label` for the inline preflight. */
|
|
@@ -2640,7 +2644,7 @@ export async function pollUserTasks(
|
|
|
2640
2644
|
// feature/plan/pr enrichment. The row's inlined `delivery-human-task__<node>` id is recognised by
|
|
2641
2645
|
// the shared `userTaskKindLabel` predicate, and buckets as `delivery` (below).
|
|
2642
2646
|
for (const run of await deliveryGraphRuns(data).all()) {
|
|
2643
|
-
if (run.process_key) subjectByInstance.set(run.process_key, { type: "delivery", key: run.run_key, title: run.title, url: null });
|
|
2647
|
+
if (run.process_key) subjectByInstance.set(run.process_key, { type: "delivery", key: run.run_key, title: run.title, url: null, humanLabels: parseHumanLabels(run.human_labels) });
|
|
2644
2648
|
}
|
|
2645
2649
|
|
|
2646
2650
|
// Per-element subject type for an ORPHANED task (no subject row) — the kind implies its aggregate even
|
|
@@ -2719,6 +2723,13 @@ export async function pollUserTasks(
|
|
|
2719
2723
|
question = readinessEscalationQuestion(subj?.waitGateLabel ?? null);
|
|
2720
2724
|
break;
|
|
2721
2725
|
}
|
|
2726
|
+
// A delivery-graph `human` node's user-task id is inlined per node (`delivery-human-task__<node>`
|
|
2727
|
+
// and its `…__esc` twin), so it can't be a static `case` above (issue #772). Fill its "Decision
|
|
2728
|
+
// context" from the run's stamped `human_labels` — otherwise the panel is blank and, because this
|
|
2729
|
+
// surface seeds no form variables, the operator has no idea what the run is waiting on.
|
|
2730
|
+
if (question === null && isDeliveryHumanElement(elementId)) {
|
|
2731
|
+
question = deliveryHumanContextQuestion(subj?.humanLabels, elementId);
|
|
2732
|
+
}
|
|
2722
2733
|
return { userTaskKey, elementId, subjectType, subjectKey, subjectTitle: subj?.title ?? null, subjectUrl: subj?.url ?? null, question, processKey: processInstanceKey, formKey: resolvedFormKey };
|
|
2723
2734
|
};
|
|
2724
2735
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nanobpm/nano-workforce",
|
|
3
|
-
"version": "0.186.
|
|
3
|
+
"version": "0.186.1",
|
|
4
4
|
"description": "Nano Workforce — an Agent Graph Orchestration application for Agentic SDLC: durable BPMN processes that coordinate a graph of AI agents across the software delivery lifecycle.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "main.ts",
|
|
@@ -5,43 +5,14 @@
|
|
|
5
5
|
"components": [
|
|
6
6
|
{
|
|
7
7
|
"type": "text",
|
|
8
|
-
"text": "### Delivery graph — node `{{nodeId}}`",
|
|
9
|
-
"conditional": {
|
|
10
|
-
"hide": "=(nodeId = null) or (nodeId = \"\")"
|
|
11
|
-
}
|
|
12
|
-
},
|
|
13
|
-
{
|
|
14
|
-
"type": "textarea",
|
|
15
|
-
"key": "prompt",
|
|
16
8
|
"label": "Now do this",
|
|
17
|
-
"
|
|
18
|
-
"readonly": true
|
|
19
|
-
},
|
|
20
|
-
{
|
|
21
|
-
"type": "text",
|
|
22
|
-
"text": "**Emits:** {{emitLabel}} — enter its typed value below (validated against the node's declared fact).",
|
|
23
|
-
"conditional": {
|
|
24
|
-
"hide": "=emitMode != \"typed\""
|
|
25
|
-
}
|
|
9
|
+
"text": "This task's instruction is shown in the Decision context above."
|
|
26
10
|
},
|
|
27
11
|
{
|
|
28
12
|
"type": "textfield",
|
|
29
13
|
"key": "value",
|
|
30
|
-
"label": "Emitted value",
|
|
31
|
-
"description": "The typed value this step hands forward to its downstream dependents.
|
|
32
|
-
"conditional": {
|
|
33
|
-
"hide": "=emitMode != \"typed\""
|
|
34
|
-
},
|
|
35
|
-
"validate": {
|
|
36
|
-
"required": true
|
|
37
|
-
}
|
|
38
|
-
},
|
|
39
|
-
{
|
|
40
|
-
"type": "text",
|
|
41
|
-
"text": "_This step emits no typed fact (N/A) — just complete it to unblock its dependents._",
|
|
42
|
-
"conditional": {
|
|
43
|
-
"hide": "=emitMode = \"typed\""
|
|
44
|
-
}
|
|
14
|
+
"label": "Emitted value (only if this step emits a fact)",
|
|
15
|
+
"description": "The typed value this step hands forward to its downstream dependents. Leave blank for a click-done step that emits nothing."
|
|
45
16
|
},
|
|
46
17
|
{
|
|
47
18
|
"type": "textarea",
|