@smartkomm/plexus-cli 0.0.0-stage → 0.1.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.
Files changed (82) hide show
  1. package/README.md +863 -2
  2. package/dist/cli.mjs +5432 -0
  3. package/dist/skill/.managed-by-plexus-cli +1 -0
  4. package/dist/skill/SKILL.md +224 -0
  5. package/dist/skill/references/accounts/email-changes.md +46 -0
  6. package/dist/skill/references/agents/use-shared-tools.md +52 -0
  7. package/dist/skill/references/applications/inspect-process-store.md +61 -0
  8. package/dist/skill/references/applications/start-a-test-application.md +66 -0
  9. package/dist/skill/references/audit/answer-access-request.md +115 -0
  10. package/dist/skill/references/config/attach-a-template.md +117 -0
  11. package/dist/skill/references/config/init-tenant.md +121 -0
  12. package/dist/skill/references/config/onboard-tenant.md +71 -0
  13. package/dist/skill/references/config/rename-program-stage.md +99 -0
  14. package/dist/skill/references/config/set-tenant-branding.md +150 -0
  15. package/dist/skill/references/config/set-tenant-terms.md +50 -0
  16. package/dist/skill/references/config/set-up-billing-catalog.md +73 -0
  17. package/dist/skill/references/config/update-tenant-config.md +174 -0
  18. package/dist/skill/references/config/wipe-tenant-config.md +107 -0
  19. package/dist/skill/references/export/create-named-export.md +47 -0
  20. package/dist/skill/references/feedback/create-and-triage-feedback.md +69 -0
  21. package/dist/skill/references/feedback/manage-feedback-tags.md +57 -0
  22. package/dist/skill/references/finance/change-contract-grant.md +36 -0
  23. package/dist/skill/references/finance/record-submission-date.md +33 -0
  24. package/dist/skill/references/interessenten/list-interessenten.md +54 -0
  25. package/dist/skill/references/marketing/demo-video-links.md +101 -0
  26. package/dist/skill/references/notifications/debug-mail-delivery.md +77 -0
  27. package/dist/skill/references/notifications/mail-level.md +90 -0
  28. package/dist/skill/references/notifications/reword-a-letter.md +155 -0
  29. package/dist/skill/references/notifications/trace-a-login.md +50 -0
  30. package/dist/skill/references/offer/add-workflow-node.md +268 -0
  31. package/dist/skill/references/offer/addresses.md +129 -0
  32. package/dist/skill/references/offer/appointment-task.md +79 -0
  33. package/dist/skill/references/offer/assignment-rule.md +195 -0
  34. package/dist/skill/references/offer/attach-form-to-workflow.md +79 -0
  35. package/dist/skill/references/offer/author-form-scenarios.md +97 -0
  36. package/dist/skill/references/offer/author-manifest-meta.md +94 -0
  37. package/dist/skill/references/offer/author-smartform.md +102 -0
  38. package/dist/skill/references/offer/bind-node-input.md +76 -0
  39. package/dist/skill/references/offer/completeness-check.md +137 -0
  40. package/dist/skill/references/offer/countersign-node.md +91 -0
  41. package/dist/skill/references/offer/create-pedagogy-unit.md +55 -0
  42. package/dist/skill/references/offer/declare-node-reads.md +99 -0
  43. package/dist/skill/references/offer/edit-live-tenant-offer.md +144 -0
  44. package/dist/skill/references/offer/edit-smartform.md +121 -0
  45. package/dist/skill/references/offer/edit-workflow-node.md +126 -0
  46. package/dist/skill/references/offer/engage-push-node.md +147 -0
  47. package/dist/skill/references/offer/insert-and-rewire-node.md +67 -0
  48. package/dist/skill/references/offer/node-tasks.md +162 -0
  49. package/dist/skill/references/offer/optional-assessment.md +65 -0
  50. package/dist/skill/references/offer/publish-offer-bundle.md +117 -0
  51. package/dist/skill/references/offer/set-offer-capacities.md +78 -0
  52. package/dist/skill/references/offer/staff-input-if-node.md +112 -0
  53. package/dist/skill/references/offer/task-appointment-result.md +69 -0
  54. package/dist/skill/references/offer/task-family-request.md +69 -0
  55. package/dist/skill/references/offer/task-result-files.md +52 -0
  56. package/dist/skill/references/offer/template-document-node.md +113 -0
  57. package/dist/skill/references/offer/workflow-default-channels.md +69 -0
  58. package/dist/skill/references/resolver/manage-contract-resolver.md +161 -0
  59. package/dist/skill/references/retention/erase-due-applications.md +99 -0
  60. package/dist/skill/references/retention/erase-due-parties.md +107 -0
  61. package/dist/skill/references/roles/migrate-parent-assignments.md +31 -0
  62. package/dist/skill/references/signing/README.md +113 -0
  63. package/dist/skill/references/signing/connect-a-school.md +90 -0
  64. package/dist/skill/references/signing/rotate-the-key.md +41 -0
  65. package/dist/skill/references/stammdaten/purge-deleted-person.md +68 -0
  66. package/dist/skill/references/stripe/connect-a-school.md +107 -0
  67. package/dist/skill/references/stripe/request-and-verify-a-payment.md +78 -0
  68. package/dist/skill/references/test-actions/drive-a-test-application.md +69 -0
  69. package/dist/skill/references/users/add-platform-admin.md +59 -0
  70. package/dist/skill/references/users/add-platform-user.md +59 -0
  71. package/dist/skill/references/users/add-tenant-admin.md +75 -0
  72. package/dist/skill/references/users/change-login-email.md +55 -0
  73. package/dist/skill/references/users/cli-access-for-tenant-admin.md +117 -0
  74. package/dist/skill/references/users/delete-permission-group.md +76 -0
  75. package/dist/skill/references/users/grant-cross-tenant-access.md +57 -0
  76. package/dist/skill/references/users/grant-feedback-assignable.md +43 -0
  77. package/dist/skill/references/users/set-password.md +57 -0
  78. package/dist/skill/references/users/use-test-account.md +79 -0
  79. package/dist/skill/references/worker-ops/debug-worker-jobs.md +90 -0
  80. package/dist/skill/references/worker-ops/verify-migrations.md +51 -0
  81. package/dist/spec.snapshot.json +156821 -0
  82. package/package.json +53 -4
@@ -0,0 +1,268 @@
1
+ # Add a workflow node to an offer bundle
2
+
3
+ **When to use:** an offer's admission workflow needs another step (an approval
4
+ gate, a waitlist holding node, a selection terminus, another form, …) and you want
5
+ the graph + progress board kept in sync automatically — instead of hand-editing
6
+ `workflow.json` (graph + `progressWiring.mappings`) and `manifest.json`
7
+ (`progressLayout` states + columns) in lockstep.
8
+
9
+ ## Prerequisites
10
+ - **Env / scope:** none for authoring — `offer workflow node add|rm|list` and
11
+ `offer workflow show` are **offline** (read/write the on-disk bundle, no
12
+ deployment). Only the final `offer apply --publish` is online and needs
13
+ `--env <env>` + a token with offer config rights (`plexus-cli --env <env> whoami`).
14
+ - **A bundle on disk:** scaffold one with `offer init`, or `offer export` an
15
+ existing offer into a directory.
16
+ - **Tooling:** an installed, current `plexus-cli` (`plexus-cli offer workflow node add --help`).
17
+ - **Both languages:** a node's label is ONE `{ de, en }` field — `--label` AND
18
+ `--label-en` are required (as `--reason` + `--reason-en` on a collectPayment
19
+ node and `<id>:<Deutsch>:<English>` per `--decision-outcomes` entry).
20
+ - **Supported types:** `form`, `decision`, `review`, `setFlag`, `approval`, `waitlist`,
21
+ `roleAssign`, `join`, `collectPayment`, `completenessCheck`, `countersign`, `engagePush`, `if`, `end`
22
+ (the list `offer workflow node add --help` prints). `decision` is the
23
+ configurable-decision builder (ATS primitives — assign-decision, composed
24
+ tasks, artifacts, decision-as-task); `approval` is a **deprecated alias** for
25
+ `decision` (maps `--decision-outcomes` → outcomes). `setFlag` flips an external
26
+ flag on the form instance it names (`--flag-key` + `--target '$node.<formNode>.admission'`).
27
+ `countersign` („Unterschrift (intern)") is the
28
+ school's countersignature of a checked contract — see
29
+ [Countersign the contract](./countersign-node.md).
30
+ `if` („Wenn") routes on one DSL expression over `$env` (`--condition
31
+ '$env.staffInput.<key>'`, outcomes `true` / `false` — route both with
32
+ `--outcomes`) — see [Branch on a staff input](./staff-input-if-node.md).
33
+ `completenessCheck` waits for outstanding documents (`--require`,
34
+ `--deadline-days`|`--deadline-until`, `--notification requested=…`, optional
35
+ `--guest-role` + `--person`) — see
36
+ [Wait for outstanding documents](./completeness-check.md). The folded
37
+ `governedDocument` (offer letter / contract) cannot be added by the CLI yet.
38
+ A `form` node binds a SmartForm via `--form` / `--new-form` (see
39
+ [Attach a form to a workflow](./attach-form-to-workflow.md)).
40
+ - **Letters with the node:** `--notification <event>=<templateId>` (repeatable)
41
+ binds a letter in the same call — required when the type declares the event
42
+ required (completenessCheck `requested`, countersign `countersigned`), since
43
+ `add` validates before writing.
44
+ - **Addresses with the node**: every flag that reads another node's data
45
+ takes a full address naming its producer, in single quotes — `--reads` /
46
+ `--require` (`'$node.<formNode>.admission.<field>'`), `--person`
47
+ (`'$node.<formNode>.admission.beneficiary'`), `--target` / `--adopt-form`
48
+ (`'$node.<formNode>.admission'`), `--contract`, engagePush `--form` / `--payment`.
49
+ A bare key is refused with the addresses visible at the new node; `add` binds
50
+ NOTHING implicitly. Bundles exported before addresses were introduced (with `wires`) are refused —
51
+ export again. See [Name what a node reads](./addresses.md).
52
+
53
+ ### Payment gate (`collectPayment`)
54
+ `--article <key>` and `--settlement online|separate` are **required** (no
55
+ default); both are written as the node's parameters (`inputs.article`,
56
+ `inputs.settlement` — the editor lists them under „Parameter"). The article is
57
+ a LINK: the key (`billing_item_definitions.configKey`) of a `one_time`
58
+ article assigned to the offer's program (Finanzen → Artikel, directly or through
59
+ an offered package). Amount, currency and the Stripe line name are read from the
60
+ article at runtime — the node stores no amount, and publish refuses `amount` /
61
+ `currency` on it (`[collectPayment/retiredField]`) as well as an unknown,
62
+ non-one-time or out-of-program article. A price change in Finanzen applies to
63
+ the next payment without a republish. `online`: the family gets a „Zahlung leisten" request row and pays
64
+ through Stripe Checkout; the process responsible can also close it
65
+ „Ohne Online-Zahlung abschließen" with a required reason (e.g. on an
66
+ installation without Stripe). `separate`: the school bills
67
+ the fee outside Plexus — nothing is asked of the family (no row, no letter; the
68
+ portal says „Die Anmeldegebühr wird separat abgerechnet."), and the process
69
+ responsible closes the step in the cockpit with **„Zahlung überspringen"**
70
+ (optional comment, feed entry „Zahlung übersprungen", nothing booked). The
71
+ routing edge is `paid` either way. Switch an existing node with
72
+ `offer workflow node set --id <node> --settlement separate` (or
73
+ `--article <key>` to charge another article); a `requested`
74
+ letter bound on a `separate` node is refused (`[notification/unknownEvent]`).
75
+ Applications already parked on the node stay on the version they are pinned
76
+ to — moving them onto the new version after `apply --publish` needs a platform
77
+ operator.
78
+
79
+ ### Terminal, fan-in, and role nodes
80
+ - **`end --outcome <outcome> --data-decision <commit|discard>`** authors a
81
+ terminal node's application outcome (`approved` | `rejected` | `withdrawn` |
82
+ `waitlist` | `expired`; default `approved`) and — required, no default —
83
+ whether the application's data is kept (`commit`) or discarded (`discard`)
84
+ at this end (the outcome is only a label and never decides it). It
85
+ writes the `terminalStatus` / `applicationOutcome` / `dataDecision` /
86
+ `terminationKind` literal inputs the end executor reads (fixing the old gap
87
+ where every CLI-authored end recorded `approved`), and contributes **one**
88
+ outcome-aware terminal column (e.g. a rejected end adds a red *Abgesagt*
89
+ column, not a misleading green *Eingeschrieben*). A second end is inserted
90
+ isolated — wire predecessors to it with `join --from` or `node set --outcomes`.
91
+ Override the ledger label with `--terminal-status`, the kind with
92
+ `--termination-kind`.
93
+ - **`join --from <nodeId>:<outcome>,…`** authors an explicit XOR fan-in
94
+ convergence. Each `--from` pair reroutes that source node's outcome onto the
95
+ join; the join's own onward edge defaults to the end node (override with
96
+ `--outcomes done:<target>`). Multi-input into a join is the one legal
97
+ many-to-one shape (the publish gate rejects multi-input on non-join nodes).
98
+ - **`roleAssign --role <key>`** grants a person-lifecycle role. `--role` is
99
+ validated **offline** against the manifest's declared `roleInstances` (fail
100
+ loud if absent — declare it first with `offer role add`). `--mode`
101
+ is `add` (default) | `remove` | `replace`; `--person '<address>'` is required —
102
+ the party that gets the role, a party leaf of a form
103
+ (`--person '$node.form.admission.beneficiary'`). Never defaulted.
104
+
105
+ ### Configurable decision node (`decision`)
106
+ The `decision` node is the **one configurable decision node** (it replaced the old
107
+ `approval` node; `--type approval` is refused as an unknown type). By default it
108
+ declares the type outcomes `approved` / `rejected` / `waitlisted`. To author a
109
+ **custom** outcome set (with your own ids + labels), pass `--decision-outcomes
110
+ <id>:<Deutsch>:<English>,…` (an outcome's label is `{ de, en }` — both languages
111
+ are required) and route each with `--outcomes`:
112
+
113
+ ```bash
114
+ # Inquiry decision: direkt aufnehmen / zum Interview / absage.
115
+ plexus-cli offer workflow node add --dir ./demo \
116
+ --type decision --id decisionInquiry --label "Erstentscheidung" --label-en "Initial decision" --after form \
117
+ --decision-outcomes 'direkt:"Direkt aufnehmen":"Admit directly",interview:"Zum Interview":"To the interview",absage:"Absage":"Rejection"' \
118
+ --outcomes 'direkt:formApplication,interview:interview,absage:joinAbsage' \
119
+ --assign-decision zulassung --decision-as-task 'Erstentscheidung treffen'
120
+ ```
121
+
122
+ Each entry becomes a `NodeOutcome {id,label}` on `node.outcomes`
123
+ (first = `isDefault`); the backend executor + publish gate + cockpit UI all validate
124
+ and render decisions against whatever set the node declares. `--decision-outcomes` is
125
+ decision-only (fails loud on any other type). Outcomes whose id maps to a known
126
+ semantic (`direkt`/`accepted`→granted, `absage`/`rejected`→rejected,
127
+ `warteliste`/`waitlisted`→waitlisted) drive the roster chip + annotation mirror; any
128
+ other id (e.g. `interview`) is a real routing decision with no typed annotation.
129
+ Route each declared outcome to a target that exists — the publish gate checks coverage.
130
+
131
+ #### ATS lifecycle primitives on a decision node
132
+ A decision node carries the full offer-lifecycle authoring surface:
133
+
134
+ - **`--assign-decision <groupKey>`** — the permission group whose members hold the
135
+ synthesized decision Work-Item (= may pick the routing outcome, in ONE act).
136
+ Pairs with `--decision-as-task` (each requires the other — fails loud). Absent
137
+ = super-user-only decide (the instance's responsible).
138
+ - **`--comment-required <id>[,<id>]`** — authored outcome ids that require a decider comment.
139
+ - **`--task …`** (repeatable) — a composed task the node spawns on enter,
140
+ instantiating a code-owned kind (the kind carries the behaviour: actions,
141
+ close protocol, respawn, gate default):
142
+ - `key:task:title:group[:dueInDays]` — an internal check task (trivial `complete`).
143
+ - `key:approval:title:group[:revisionGroup]` — an approval; gates the node by
144
+ default. With **`--task-on-reject spawnRevision`** a rejecting verdict
145
+ spawns a revision back to `revisionGroup` (then REQUIRED — fails loud).
146
+ (`binds` is DELETED — the offer arc is node-internal; use the
147
+ `offer` node type for the prepare/approve arc.)
148
+ - **`--artifact key:type:visibility:label`** (repeatable) — a versioned artifact SLOT
149
+ (`visibility` = `internal` | `external`). Each slot exposes a `<key>Uploaded` node-export.
150
+ - **Assignment rule** (GRAPH-level, no flag) — who becomes responsible and who owns
151
+ each team is `graph.assignmentRules` in the same `workflow.json`; `node add/set` do
152
+ not touch it. A node that gives work to a team the rule has no slot for makes
153
+ `offer validate` / publish refuse with `[assignment/slotMissing]` — add a
154
+ `team:<key>` list in the bundle or in the workflow editor („Team-Verantwortliche").
155
+ Recipe: `playbooks/offer/assignment-rule.md`.
156
+ - **`--decision-as-task title[:dueInDays]`** — surface the decision as a
157
+ first-class queue Work-Item. Requires `--assign-decision` (and vice versa).
158
+ Combining with gating approval tasks is fine: the HARD node gate blocks a
159
+ non-rejecting advance server-side while gate-effective rows are open.
160
+
161
+ ```bash
162
+ # Offer create-phase (mirrors the grundschule createOffer node): external artifact,
163
+ # a prepare task, a finance approval gate with a ping-pong revision on reject, and
164
+ # the Schulleitung send-offer decision row (hard-gated on the finance approval).
165
+ plexus-cli offer workflow node add --dir ./demo \
166
+ --type decision --id createOffer --label "Angebot erstellen" --label-en "Prepare offer" --after form \
167
+ --decision-outcomes 'offerReady:"Angebot freigegeben",absage:"Angebot abgebrochen"' \
168
+ --assign-decision schulleitung --decision-as-task 'Angebot versenden:3' \
169
+ --artifact 'offer:offer:external:Angebotsschreiben' \
170
+ --task 'prepareOffer:checkTask:Angebotsschreiben vorbereiten:schulleitung:5' \
171
+ --task 'offerApproval:approval:Angebot freigeben:finanzen:offer:offerUploaded:schulleitung' \
172
+ --task-on-reject spawnRevision
173
+
174
+ # Task-driven selection decision (grundschule approvalSchulleitung), gate-less:
175
+ plexus-cli offer workflow node add --dir ./demo \
176
+ --type decision --id freigabe --label "Freigabe" --label-en "Approval" --after form \
177
+ --decision-outcomes 'approved:Freigegeben,rejected:Abgelehnt,waitlisted:Warteliste' \
178
+ --assign-decision schulleitung \
179
+ --comment-required rejected \
180
+ --decision-as-task 'Über Freigabe entscheiden:5'
181
+ ```
182
+
183
+ All of the above is validated by the REAL publish gate on `offer validate`; the CLI adds
184
+ fail-loud cross-checks at the flag boundary (undeclared bound artifact, `spawnRevision`
185
+ without `revisionGroup`, `decision-as-task` ⇔ `assign-decision` pairing).
186
+
187
+ ## Steps
188
+ ```bash
189
+ # 1. Scaffold a minimal linear offer (start → form → end).
190
+ plexus-cli offer init --slug demo --title "Demo Schule" --title-en "Demo School" --tenant test --responsible sekretariat --out ./demo
191
+
192
+ # 2. Inspect the graph.
193
+ plexus-cli offer workflow node list --dir ./demo
194
+ plexus-cli offer workflow show --dir ./demo
195
+
196
+ # 3. Insert a decision gate between the form and the end node.
197
+ # --outcomes routes each declared outcome; any omitted outcome defaults to
198
+ # where --after currently points (here end), splicing into the chain.
199
+ plexus-cli offer workflow node add --dir ./demo \
200
+ --type decision --id approval --label "Freigabe" --label-en "Approval" --after form \
201
+ --assign-decision freigeber --decision-as-task 'Freigabe entscheiden' \
202
+ --outcomes approved:end,rejected:end
203
+
204
+ # 4. Validate offline against the REAL publish gate (add already ran it; this is
205
+ # the explicit re-check / CI gate).
206
+ plexus-cli offer validate --dir ./demo
207
+
208
+ # 5. Apply + publish (online). Needs --env + a token; cuts new versions on diff.
209
+ plexus-cli --env <env> --tenant test offer apply --dir ./demo --publish
210
+ ```
211
+
212
+ - **`--id`** is optional — defaults to the type, numbered from 2 when taken
213
+ (`approval`, `approval2`, …; `makeNodeId`). An id you pass must be a dot-free
214
+ identifier (`^[a-z][A-Za-z0-9]*$`, e.g. `decisionInquiry`) — `offer validate`
215
+ refuses anything else as `[graph/nodeIdFormat]`.
216
+ - **`--after <nodeId>`** inserts after that node and rewires its onward edge(s)
217
+ to the new node. Omit it to insert immediately before the end node.
218
+ - **`--type form`** binds a SmartForm — `--form <slug>` (existing) or
219
+ `--new-form --form <slug>` (scaffold + bind). See the dedicated playbook.
220
+ - **Remove** a node (rewires predecessors onto its single successor, strips its
221
+ progress entries, re-validates; refused while an address elsewhere names it):
222
+ ```bash
223
+ plexus-cli offer workflow node rm --dir ./demo --id approval
224
+ ```
225
+
226
+ ## Verify
227
+ ```bash
228
+ plexus-cli offer workflow node list --dir ./demo # the node appears, routing is wired
229
+ plexus-cli offer validate --dir ./demo # exit 0 = publish gate clean
230
+ ```
231
+
232
+ For the approval example, `offer validate` exits 0 with **no manual manifest
233
+ edit**: `offer workflow node add` materialised the `approval.released` process
234
+ state, its board column, and the `(approval, released) → process/approval.released`
235
+ wiring mapping. A decision that reads nothing of the form names no address
236
+ (`offer workflow node reads --dir ./demo --node approval` lists nothing). After `offer workflow node rm`, the graph returns to the linear
237
+ `form → end` shape and validation passes again.
238
+
239
+ ## Idempotency / gotchas
240
+ - **`add` validates before writing.** Any error-severity issue from the real
241
+ publish gate (`runWorkflowValidators` etc.) blocks the write and exits 1 — the
242
+ bundle is left untouched. A failed `add` never leaves a half-spliced bundle.
243
+ - **Splice defaults can over-route.** Every declared outcome you DON'T name in
244
+ `--outcomes` defaults to the anchor's current onward target. Inserting a
245
+ multi-outcome node (endauswahl, approval) after a node that points at the end
246
+ node routes *all* its outcomes to the end node — pass `--outcomes` to fan them
247
+ out (e.g. `warteliste:waitlist`).
248
+ - **Nothing is bound implicitly.** A node reads a form only at the address it
249
+ names (`$node.<formNode>.admission.<field>`), and the producer must run on every
250
+ path before it (`[address/notVisible]` otherwise). A `form` node's `smartForm` is
251
+ written as a `{ kind: "literal", value: <slug> }`; with
252
+ `--adopt-form '$node.<formNode>.admission'` it continues that instance instead
253
+ (`adoptForm` holds the address; the adopting node produces nothing — later
254
+ nodes still read the creator's address). Explicit-by-default, matching `offer init`.
255
+ - **`rm` refuses** to remove the start (`eventTrigger`) or `end` node, a node with
256
+ multiple distinct successors (ambiguous splice), and a node any address elsewhere
257
+ still names (its form instance, document, contract, payment, or a value one of
258
+ its tasks writes) — the refusal lists every naming place (config path +
259
+ address); edit those first. Orphans surface at re-validate and block the write.
260
+ - **Progress labels/tones** come from one built-in default table, shared with the
261
+ `offer init` scaffold. An event with no default falls back to its name +
262
+ neutral tone and prints a warning.
263
+
264
+ ## Related
265
+ - [Attach a form to a workflow](./attach-form-to-workflow.md) — the `--type form` flow.
266
+ - [Author a SmartForm as code](./author-smartform.md) — build the form itself.
267
+ - `plexus-cli offer --help` — the full `offer …` command reference; `offer validate`
268
+ runs the same publish-gate validators as `offer workflow node add`.
@@ -0,0 +1,129 @@
1
+ # Name what a node reads (addresses)
2
+
3
+ **When to use:** a node must read data another node produces — the form's answers in a
4
+ contract, a value a review task records, a derived price, the contract a countersignature books
5
+ on — or the gate refuses a bundle with an `[address/*]` code. A read **names its
6
+ producer** in the field that reads it: a full address. There are no data edges any more
7
+ (`node.wires` is gone), no form implied by the node and no bare keys. Control edges (`next`) only
8
+ route. What a node may read is what is produced on every path before it.
9
+
10
+ ## Prerequisites
11
+ - An offer bundle on disk (`offer init`, or `offer export --slug <slug> --out <dir>`; a live
12
+ tenant's bundle only into a private scratch dir — see
13
+ [Edit a live tenant's offer](./edit-live-tenant-offer.md)). Fully offline: every verb runs the
14
+ publish gate on the result and writes nothing when the edit introduces an error.
15
+ - **A bundle exported before addresses were introduced is refused** (`[graph/retiredField]` for `wires`, a binding
16
+ with `path`, `adoptForm: true`; `[address/bareIdentifier]` for a bare key). Do not patch it by
17
+ hand — export it again (the server migration folded the live graph onto addresses).
18
+ - The spelling — **always in single quotes on the command line**, or the shell expands `$node`:
19
+
20
+ | Address | Names | Example (demo tenant `test`) |
21
+ |---|---|---|
22
+ | `$node.<formNode>.admission` | the form instance a form node creates | `$node.form.admission` |
23
+ | `$node.<formNode>.admission.<field>` | one field (or export, or party leaf `beneficiary` / `signers`) of it | `$node.form.admission.kind.geburtsurkunde` |
24
+ | `$node.<node>.document` | the newest revision of a document node's slot | `$node.platzangebot.document` |
25
+ | `$node.<node>.contract` | a machine-produced contract (`production: resolver`) | `$node.contract.contract` |
26
+ | `$node.<node>.payment` | a payment node's result | `$node.zahlung.payment` |
27
+ | `$task.<task>.<key>` | the value the ONE task `<task>` writes under store key `<key>` | `$task.reducedFee.reducedFee` |
28
+ | `$derived.<key>` | a computed price (`processStore[].derive`) | `$derived.schulgeld` |
29
+ | `$env.applicant.language` | a fact about the application nobody produces | |
30
+
31
+ An adopting form node (`adoptForm`, e.g. `formZusage` in the demo tenant `test`) **produces nothing**: the
32
+ instance stays its creator's, so every later node still reads `$node.form.admission.…`.
33
+
34
+ ## Steps
35
+ ```bash
36
+ # What arrives at a node: every address it reads, where it is written, who produces it —
37
+ # including what it reads by carrying (the evidence of a task whose value it reads, the
38
+ # sources of a derived price, the price check):
39
+ plexus-cli offer workflow node reads --dir <dir> --node contract
40
+
41
+ # A bare key is refused with the list of what that node can read — use it to pick:
42
+ plexus-cli offer workflow node set-reads --dir <dir> --node contract --address kind.vorname
43
+ # → --address (reads) "kind.vorname": ein nackter Schlüssel …
44
+ # An „Vertrag" (contract) lesbar:
45
+ # Bewerbung Grundschule Havelland › Anmeldung ($node.form.admission)
46
+ # Prüfung › Schulgeld mit Ermäßigung › Einkommensermäßigung ($task.reducedFee.reducedFee)
47
+ # …
48
+
49
+ # The flags that read data (each takes full addresses):
50
+ plexus-cli offer workflow node set-input --dir <dir> --node contract --input schulgeld --address '$derived.schulgeld'
51
+ plexus-cli offer workflow node set-reads --dir <dir> --node contract --from form --field kind.vorname,kind.nachname
52
+ plexus-cli offer workflow node set-requires --dir <dir> --node nachreichen --address '$node.form.admission.kind.masernschutz'
53
+ plexus-cli offer workflow node task set --dir <dir> --node reviewEingereicht --key reducedFee \
54
+ --when '$node.form.admission.ermaessigungBeantragt' --reads '$node.form.admission.gebuehren.einkommensnachweise'
55
+ plexus-cli offer workflow node add --dir <dir> --type setFlag ... --target '$node.form.admission'
56
+ plexus-cli offer workflow node add --dir <dir> --type form ... --adopt-form '$node.form.admission'
57
+ plexus-cli offer workflow node add --dir <dir> --type countersign ... --contract '$node.contract.contract'
58
+ ```
59
+
60
+ The short form `--from <formNode> --field <path>` (set-input, `*-reads`, `*-requires`) only
61
+ composes `$node.<formNode>.admission.<path>` — it names the node, it never picks one.
62
+
63
+ Which flag sets which field:
64
+
65
+ | Field | Verb / flag |
66
+ |---|---|
67
+ | an input binding | `node set-input --address` (or `--from/--field`) |
68
+ | `reads` (decision / review / governedDocument) | `node set-reads|add-reads|remove-reads --address`; review at add: `node add --reads` |
69
+ | `requires` (completenessCheck) | `node set-requires|add-requires|remove-requires --address`; at add: `--require` |
70
+ | setFlag `target` | `node add|set --target` |
71
+ | form `adoptForm` | `node add --adopt-form` |
72
+ | resolver governedDocument / engagePush `form` | `node set --form '<instance address>'` (engagePush also at add) |
73
+ | countersign / engagePush `contract`, engagePush `payment` | `node add|set --contract|--payment` |
74
+ | roleAssign / completenessCheck `person` | `node add|set --person` |
75
+ | task `when`, `reads`, `documents` | `node task add|set --when|--reads|--documents` (`field:<address>`, `document:$node.<node>.document`; an own slot stays `artifact:<key>`) |
76
+ | task family request | `--family-request-fields <addresses>` or `--family-request-from '$node.<form>.admission'` |
77
+ | record field suggestion | `--field '{…,"suggest":{"kind":"form","address":"$node.form.admission.alleinerziehend"}}'` |
78
+ | `pricingRules[].rows[].when`, `processStore[].derive.reduction` | **no CLI verb** — edit the bundle JSON; a condition's identifiers are canonical addresses (`$task.geschwisterrang.geschwisterRangSchule >= 3`), a reduction is `{ "scope": "task", "owner": "reducedFee", "key": "reducedFee" }` |
79
+
80
+ ## In the IDE instead
81
+ The Workflow-IDE offers exactly what the gate accepts — the same rules
82
+ (`addressFieldOptions`), one picker in every editor, never a typed key:
83
+ - **What arrives at a node:** select it — the bottom bar's „Eingang" column lists
84
+ its reads and what it may read (Formular · Dokumente · Zahlungen · Aufgaben ·
85
+ Abgeleitet · Umgebung), each with producer, address and how it is read; a rose
86
+ entry is a read publish refuses (`[address/notVisible]`). Drag an entry onto a
87
+ parameter to bind it.
88
+ - **Node fields** (`reads`, `requires`, setFlag `target`, `adoptForm`, `form` /
89
+ `contract` / `payment`): node → „Details" → „Einstellungen".
90
+ - **Task fields** (`when`, `reads`, `documents`, `familyRequest`, a record's
91
+ suggestion): node → „Details" → „Team".
92
+ - **Pricing-rule conditions and reductions:** header → „Preisregeln" („Adresse
93
+ einfügen" puts the `$task…` identifier into the condition).
94
+
95
+ ## Verify
96
+ ```bash
97
+ plexus-cli offer validate --dir <dir> # exit 0, no [address/*] error
98
+ plexus-cli offer workflow node reads --dir <dir> --node <id> --json
99
+ plexus-cli --env staging --tenant <t> offer plan --dir <dir> # ~ workflow; address:/reads: rows against the live head (see edit-live-tenant-offer.md)
100
+ ```
101
+
102
+ ## Idempotency / gotchas
103
+ - **The gate judges every address** before writing: `[address/shape]`, `[address/bareIdentifier]`,
104
+ `[address/unknownOwner]` (no such node / task), `[address/unknownKey]` (the node does not
105
+ produce it — a form produces `admission`, a resolver contract `contract`, not `document`),
106
+ `[address/ownerNotWriter]` (the task does not write that store key), `[address/notVisible]`
107
+ (the producer is not on EVERY path before the reader — on a detour the reader would wait
108
+ forever), `[address/pathNotInTarget]` (the form has no such field), `[address/typeMismatch]`
109
+ (a `when` must be a yes/no export, a binding must fit its slot), `[address/notFormInstance]`
110
+ (`target`, `adoptForm`, `form`, `familyRequest.from` name `$node.<creating form>.admission`
111
+ without a path), `[address/mixedForms]` (one list — node `reads`, `requires`, a task's
112
+ `when` + `reads`, `familyRequest.fields` — names ONE form), `[address/missing]` (setFlag
113
+ `target`, countersign `contract`, a resolver's / engagePush's `form`). Only errors the edit
114
+ INTRODUCES block it; errors the bundle already had are printed as „vorbestehend".
115
+ - **A node reads its own results** (a value one of its tasks writes, its own document slot)
116
+ without dominance; it is still in its read list.
117
+ - **`node rm` refuses while an address names the node** — its form instance, document,
118
+ contract, payment, or a value one of its tasks writes — and lists every place (config path +
119
+ address). Edit those first; nothing is dropped silently.
120
+ - **Renaming a task key or moving a writing task to another node** changes the address
121
+ (`$task.<task>.<key>`): every reader must be edited, and running applications cannot re-pin
122
+ across it.
123
+ - Nothing reaches a live process until `offer apply --publish`; running applications keep their
124
+ pinned version.
125
+
126
+ ## Related
127
+ - [Bind a node input](./bind-node-input.md) · [Declare what a document node reads](./declare-node-reads.md)
128
+ - [Add a workflow node](./add-workflow-node.md) · [Countersign the contract](./countersign-node.md) · [Node tasks](./node-tasks.md)
129
+ - [Edit a live tenant's offer](./edit-live-tenant-offer.md) — export, plan and apply against a live tenant
@@ -0,0 +1,79 @@
1
+ # Give an appointment its Terminaufgabe („Taster Day durchführen")
2
+
3
+ **When to use:** a „Termin vereinbaren" task books an appointment (see
4
+ [Make a task close with an appointment](./task-appointment-result.md)), and the work
5
+ done AT that appointment — hold the interview, run the taster day, give feedback with
6
+ attachments — belongs to whoever hosts it, due a few days after it ends.
7
+
8
+ ## The model in three sentences
9
+
10
+ The **Terminaufgabe** is an ordinary task of the same node with
11
+ `appointment: { of: '<booking task>', dueDaysAfterEnd: N }`. It spawns with its round
12
+ and waits („Wartet auf: <booking task>") until the booking binds it: its holder is the
13
+ appointment's **host** (a person of the task's team, or that team's owner on the
14
+ application), its due day the appointment's end + N. It closes with its result — the
15
+ feedback, a recommendation / choice / free text with `files: true` — and holds whatever
16
+ it `blocks` until then.
17
+
18
+ ## Prerequisites
19
+
20
+ - The booking task exists with `result.kind: appointment`.
21
+ - The Terminaufgabe's team (`--team`) is the team the hosts come from: the booking
22
+ dialog offers only that team and its members, and the server refuses others.
23
+ Teachers who host need cockpit access (`bewerbungsmanagement.applications.read`,
24
+ `aufgaben.items.read/write`).
25
+ - The target environment runs a platform version that knows Terminaufgaben (`offer apply`
26
+ publishes against the deployed gate; if publish does not know `appointment`, ask the
27
+ platform operator).
28
+
29
+ ## Steps
30
+
31
+ ```bash
32
+ # 1. The booking task no longer blocks the follow-up — the hold is derived.
33
+ plexus-cli offer workflow node task set --dir $WORK/$S --node decision --key tasterDayTermin --clear-blocks
34
+ # 2. The Terminaufgabe: team, link, due rule, feedback with files.
35
+ plexus-cli offer workflow node task add --dir $WORK/$S --node decision --key tasterDayDurchfuehren \
36
+ --title 'Taster Day durchführen' --title-en 'Hold the taster day' --team teachers \
37
+ --appointment-of tasterDayTermin --due-days-after-end 2 \
38
+ --result recommendation --chat all --files
39
+ # 3. If the booking task is part of an optional action, the Terminaufgabe joins it —
40
+ # and the action is repeatable: a cancelled appointment ends the round.
41
+ plexus-cli offer workflow node optional-action set --dir $WORK/$S --node decision --key tasterDay \
42
+ --tasks tasterDayTermin,tasterDayDurchfuehren --repeatable
43
+ plexus-cli offer validate --dir $WORK/$S
44
+ ```
45
+
46
+ Then the live path ([Edit a live tenant's offer](./edit-live-tenant-offer.md)):
47
+ `--env prod offer plan` → `--env prod offer apply --allow-prod --publish` → plan again
48
+ until every row is `same`.
49
+
50
+ ## Verify
51
+
52
+ - `offer validate` shows no `[workItems/appointment*]` error.
53
+ - In the cockpit: trigger the action → the Terminaufgabe reads „Wartet auf: …";
54
+ book with a host of the team → the card shows the appointment line
55
+ („Taster Day · Di., 21.10. 09:00–12:00 · Raum 2 · Gastgeber: …") and the host holds it.
56
+
57
+ ## Gotchas
58
+
59
+ - Publish refuses: `of` naming no appointment task of the same node
60
+ (`appointmentOfUnknown`), two Terminaufgaben for one booking task (`appointmentOfTwice`),
61
+ `--due-in-days` beside the end rule or N outside 0–60 (`appointmentTaskDue`), any
62
+ `blocks` aimed at it (`appointmentTaskBlocked`), a different optional action than the
63
+ booking task (`appointmentTaskBinding`), an optional action spawning the booking task
64
+ without `repeatable: true` (`appointmentActionRepeatable` — a cancellation ends the
65
+ round, and without a second one a decision waiting for the action via `requiresOneOf`
66
+ would hang for good), an approval / `when` / `optional` /
67
+ family request / missing result (`appointmentTaskShape`). Warning
68
+ `appointmentChatUnreadable`: the feedback chat is another team's — the host may not
69
+ read it; prefer `all` or the task's own team.
70
+ - Holder and due day have ONE writer, the appointment: „Zuweisen" refuses a bound
71
+ Terminaufgabe, and so does „Wiederöffnen" (edit the feedback text instead).
72
+ - A staff member who hosts an appointment with an open Terminaufgabe cannot be
73
+ withdrawn (`[appointment/hostInUse]`) — change the appointment's host first.
74
+ - Running applications keep their pinned version.
75
+
76
+ ## Related
77
+
78
+ - [Make a task close with an appointment](./task-appointment-result.md)
79
+ - [Files on a task result](./task-result-files.md)