@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,126 @@
1
+ # Edit a workflow node
2
+
3
+ **When to use:** change an existing node in an offer's workflow — relabel it, change
4
+ who decides on a decision node, rebind a form node to a different
5
+ SmartForm, or rewire where an outcome routes — without removing and re-adding it.
6
+
7
+ ## Prerequisites
8
+ - An offer bundle on disk carrying a `workflow.json` (`offer init …` or `offer export …`).
9
+ - Authoring is **fully offline**; only `offer apply --publish` needs an API token of a
10
+ user who may configure the tenant's offers (a tenant admin — see
11
+ [Add a tenant admin](../users/add-tenant-admin.md)).
12
+ - An installed, current `plexus-cli` (`plexus-cli offer workflow node set --help`).
13
+
14
+ ## Steps
15
+ ```bash
16
+ # Relabel a node — its label is ONE { de, en } field. A German-only change
17
+ # refuses while a real English exists (pass --label-en too); --label-en alone
18
+ # re-translates the current German.
19
+ plexus-cli offer workflow node set --dir <bundle> --id form --label "Bewerbungsformular" --label-en "Application form"
20
+
21
+ # Decision node: hand the decision row to another team (permission-group key) and
22
+ # name the outcomes that need a comment (there are no approver slots).
23
+ plexus-cli offer workflow node set --dir <bundle> --id freigabe \
24
+ --assign-decision schulleitung --comment-required rejected
25
+
26
+ # Form node: rebind to a different carried SmartForm.
27
+ plexus-cli offer workflow node set --dir <bundle> --id form --form <other-form-slug>
28
+
29
+ # Rewire routing: send an outcome to a different existing node.
30
+ plexus-cli offer workflow node set --dir <bundle> --id form --outcomes "standard:freigabe"
31
+
32
+ # End node: re-author the terminal outcome (re-syncs the terminal progress column).
33
+ plexus-cli offer workflow node set --dir <bundle> --id endRejected --outcome rejected
34
+
35
+ # End node: keep or discard the application's data at this end. Changing
36
+ # --outcome never changes it; a node without one must be given it here.
37
+ plexus-cli offer workflow node set --dir <bundle> --id endRejected --data-decision discard
38
+
39
+ # roleAssign node: change the granted role / mode; the person is an address.
40
+ plexus-cli offer workflow node set --dir <bundle> --id roleAssignSchueler --role schueler --mode add \
41
+ --person '$node.form.admission.beneficiary'
42
+
43
+ # What a node reads is named by address (single quotes — the shell expands $):
44
+ # setFlag: the instance it raises the flag on; countersign / engagePush: the
45
+ # contract; engagePush: the payment; a resolver contract / engagePush: the form
46
+ # instance (`--form` takes a SmartForm slug only on a FORM node).
47
+ plexus-cli offer workflow node set --dir <bundle> --id setFlagZusage --target '$node.form.admission'
48
+ plexus-cli offer workflow node set --dir <bundle> --id gegenzeichnung --contract '$node.contract.contract'
49
+ plexus-cli offer workflow node set --dir <bundle> --id contract --form '$node.form.admission'
50
+ plexus-cli offer workflow node set --dir <bundle> --id engagePush --payment '$node.zahlung.payment'
51
+ # A bare key is refused with the addresses visible at the node — see
52
+ # [Name what a node reads](./addresses.md). Check the result with
53
+ # plexus-cli offer workflow node reads --dir <bundle> --node <id>
54
+
55
+ # Document node (governedDocument): contract without a Freigabe, asking before it
56
+ # goes out („„Vertrag" jetzt senden?" on „Bereitstellen"). `--approval none` drops
57
+ # assign.releaseApproval + titles.approve; `--approval release` restores a
58
+ # Freigabe (title via --approve-title, default "<Dokument> freigeben") and drops
59
+ # the confirmation. Same edits as the IDE's Knotendetails → Einstellungen.
60
+ plexus-cli offer workflow node set --dir <bundle> --id contract --approval none --confirm-issue true
61
+
62
+ # Document node that the family returns signed (signedUpload / signedUploadChecked):
63
+ # „Bestätigung der Vertragsbedingungen" is ON by default — before the
64
+ # signed copy is uploaded the uploader confirms the terms, the protocol records
65
+ # who and when. Switch it off, or reword the question in BOTH languages
66
+ # ("" "" restores the default wording):
67
+ plexus-cli offer workflow node set --dir <bundle> --id contract --confirm-terms false
68
+ plexus-cli offer workflow node set --dir <bundle> --id contract \
69
+ --terms-question-de "Haben Sie den Schulvertrag gelesen und verstanden?" \
70
+ --terms-question-en "Have you read and understood the school contract?"
71
+
72
+ # Decision node: no outcome until ONE of its optional actions is COMPLETED —
73
+ # e.g. „Interview oder Taster Day vor der Grundsatzentscheidung". The keys are
74
+ # `optionalActions` keys of THIS node (add the actions first); a foreign or
75
+ # unknown key fails the publish gate ([decision/requiresOneOfUnknownAction]).
76
+ # `--requires-one-of ""` drops the rule. Only on `node set`: the actions must exist.
77
+ plexus-cli offer workflow node set --dir <bundle> --id decision --requires-one-of interview,tasterDay
78
+
79
+ # Then validate + publish (publishing re-versions the workflow).
80
+ plexus-cli offer validate --dir <bundle>
81
+ plexus-cli --env <env> offer apply --publish --dir <bundle>
82
+ ```
83
+
84
+ ## Verify
85
+ ```bash
86
+ plexus-cli offer workflow node list --dir <bundle> # label / outcomes / routing reflect the edit
87
+ plexus-cli offer validate --dir <bundle> # "local validation clean"
88
+ ```
89
+
90
+ ## Idempotency / gotchas
91
+ - **Validate-before-write.** Every `node set` runs the real publish gate before
92
+ touching disk — on any error (bad routing target, unknown outcome, …) it
93
+ writes **nothing**.
94
+ - **A bundle exported before addresses were introduced is refused** (`[graph/retiredField]`: `wires`, a binding
95
+ with `path`, `adoptForm: true`) — export it again; never patch it by hand.
96
+ - **`--type` is not editable.** A type change rewires outcomes/specs/progress, so it is
97
+ a remove + add: `offer workflow node rm` then `offer workflow node add`. Passing
98
+ `--type` to `set` fails loud with that guidance.
99
+ - **Routing is validated at the boundary:** the outcome must be one the node declares
100
+ (or the implicit `done` for a no-outcome node), and the target must be an existing
101
+ node id — else it fails loud and writes nothing.
102
+ - **Approver group existence is server-side.** The CLI authors the
103
+ `permissionGroupKey`; whether it resolves to a real tenant permission group is checked
104
+ at `apply --publish` / runtime, not offline.
105
+ - **Relabel never touches progress.** Progress state/column labels come from the node's
106
+ events, not its label, so a `set` leaves the progress layout/wiring untouched.
107
+ - **`--confirm-issue true` needs approval `none`.** With a Freigabe, „Freigabe
108
+ anfordern" sends nothing to the family and the Freigabe is itself the check — the
109
+ flag would be dead wiring, so the CLI refuses it (and publish would too).
110
+ - **`--confirm-terms` / `--terms-question-*` only on a signing node.** A `confirm`
111
+ document (Platzangebot) has no signed copy to upload — the CLI refuses it, publish
112
+ too (`[governedDocument/deadWiring]`); so does a reworded question while the
113
+ confirmation is off.
114
+ - **`--requires-one-of` holds the whole decision group** — Freigeben, Warteliste and
115
+ Ablehnen alike, for everyone incl. the process responsible; only „Bewerbung
116
+ ablehnen" (reject-anytime) stays open. Running applications on the old version are
117
+ not affected until they are re-pinned.
118
+ - **Re-publishing = a new workflow version.** `offer apply --publish` after an edit cuts
119
+ a new `workflow_def_versions` row (and re-pins the smartform versions).
120
+
121
+ ## Related
122
+ - [Add a workflow node](./add-workflow-node.md) — splice a new node (incl. the
123
+ `contractCreate`/`contractSign` mockup pair).
124
+ - [Attach a form to a workflow](./attach-form-to-workflow.md) — bind a form at add time.
125
+ - [Name what a node reads](./addresses.md) — the address flags and `node reads`.
126
+ - Command reference: `plexus-cli offer workflow --help`.
@@ -0,0 +1,147 @@
1
+ # Push accepted applications to Engage („Engage-Übertragung", TEMPORARY)
2
+
3
+ **When to use:** a school that still runs Engage SMIS wants each accepted
4
+ application transferred once, one-way, into Engage as a prospective pupil with its
5
+ contacts. The node `engagePush` sits right before the „angenommen" end, records
6
+ the push and goes on at once — the engine never waits for Engage. A stopped push opens
7
+ the team task „Engage-Übertragung prüfen". Each push is a document of the application,
8
+ „Übertragungsprotokoll" (Artefakte rail): every step, every request with what was
9
+ sent and what came back, and what was not sent — the acts sit in its action bar.
10
+ The node type is temporary and is removed with its slice.
11
+
12
+ ## Prerequisites
13
+ - A bundle on disk, a live tenant's only in a private scratch dir
14
+ ([Edit a live tenant's offer](./edit-live-tenant-offer.md)).
15
+ - The mapping as a JSON file (`node.engage`; `plexus-cli offer workflow node set-engage --help`).
16
+ It carries the school's Engage ids — lookup ids, AIF field ids, the admission stage,
17
+ the address format — so it lives next to the bundle, **never in version control**.
18
+ The `test` tenant carries no Engage push, so there is no example offer to export;
19
+ `offer validate` names every gap in a mapping you write.
20
+ The application's stage, school year, unit, offer or applicant come from an environment
21
+ variable by its address: `{ "from": { "address": { "scope": "env", "key": "stage.key" } }, "lookup": "yearGroup" }`
22
+ (the keys: IDE tab „Umgebungsvariablen"). An older mapping with
23
+ `{ "from": { "context": "stageKey" } }` is refused — export again.
24
+ Two answers that make ONE Engage value are joined: `{ "from": { "join": [".vorname", ".nachname"] } }`
25
+ (space-separated, in this order, empty unless every part is answered — never half a name;
26
+ a lookup then keys on the joined text, e.g. `"monatlich sepa"`). An answer the form leaves
27
+ optional but Engage requires gets an explicit Engage value: `{ "from": { "field": ".geschlecht" },
28
+ "lookup": "gender", "whenEmpty": "U" }` — sent as it stands, only for a form source and only
29
+ when the whole answer is missing (not when a `pick` finds nothing); it fills a record that
30
+ goes out, it never makes an unanswered optional record (address, e-mail, nationality …) go
31
+ out. Never use it to turn an unanswered (possibly hidden) yes/no question into „no".
32
+ The mapping is Engage vocabulary: a body field like `jobTitle` is fine (publish's i18n gate
33
+ skips `node.engage`).
34
+ - A team (permission-group key) that checks a stopped transfer.
35
+ - The deployment's Engage push mode — deployment configuration a platform operator sets,
36
+ not a CLI verb. Local and staging run `dry`; prod runs `live`, but with production not
37
+ allowed, so only the Sandbox school is written. Every push starts as a dry run; live is the cockpit's „Jetzt live übertragen"
38
+ after a clean dry run. The target `production` of the Engage connection is refused
39
+ unless the platform operator explicitly allowed production on that deployment — also
40
+ in a dry run.
41
+
42
+ ## Steps
43
+ ```bash
44
+ # 1. The accepting edge into the end:
45
+ plexus-cli offer workflow node list --dir <dir>
46
+
47
+ # 2. Splice the push onto it. It names what it reads by address (single
48
+ # quotes!): the form instance always; the contract and the payment when the
49
+ # mapping reads `{ contract: … }` / `{ payment: … }` sources. An uploaded
50
+ # contract is `$node.<contract>.document`, a generated one (`production:
51
+ # resolver`) `$node.<contract>.contract`.
52
+ plexus-cli offer workflow node add --dir <dir> --type engagePush \
53
+ --id engagePush --label "Engage-Übertragung" --label-en "Engage transfer" \
54
+ --on-edge contract:signed \
55
+ --assign-team bewerbungsteam \
56
+ --engage-target sandbox \
57
+ --engage-config <scratch>/engage-mapping.json \
58
+ --form '$node.form.admission' --contract '$node.contract.document' \
59
+ --payment '$node.collectPayment.payment'
60
+
61
+ # Change them later: node set --id engagePush --form|--contract|--payment '<address>'.
62
+ # An old bundle that still carries `wires` is refused
63
+ # ([graph/retiredField]) — export it again.
64
+
65
+ # Contract fields F01–F25 (meaning not confirmed by the school): --engage-contract-fields on
66
+
67
+ # 3. Later changes to the mapping:
68
+ plexus-cli offer workflow node set-engage --dir <dir> --node engagePush --config <scratch>/engage-mapping.json
69
+
70
+ # 4. Validate, plan, publish (publish only with the go of the process owner).
71
+ plexus-cli offer validate --dir <dir>
72
+ plexus-cli --env <env> offer plan --dir <dir>
73
+ ```
74
+
75
+ ## Verify
76
+ - `offer validate` shows no `[engagePush/*]` or `[address/*]` error (`[address/missing]`: no
77
+ `form`); `[engagePush/unmappedField]` (warning) lists mapped fields the form its `form`
78
+ address names does not have.
79
+ - `offer workflow node reads --dir <dir> --node engagePush` lists the three addresses.
80
+ - Every path into an `approved` end runs through the node (`[engagePush/bypassed]`).
81
+ - After an application reaches the node: its cockpit lists the document
82
+ „Übertragungsprotokoll · Engage · Sandbox · Probelauf" with the recorded requests; nothing
83
+ was sent. A finished dry run can run again („Probelauf wiederholen", e.g. after a form
84
+ correction) — a finished LIVE push never.
85
+
86
+ ## Go-live (platform operator only — production school, credentials)
87
+ Order matters; each step names why.
88
+ 1. **Put every offer on target `production` BEFORE the first real application starts.**
89
+ The target belongs to the PINNED version: an application started on a `sandbox` version
90
+ transfers to the Sandbox school forever, also after the offer is switched.
91
+ `node set` has no `--engage-target` flag: on an existing node, set its
92
+ `inputs.target` literal to `production` in the exported bundle, then `offer validate`
93
+ and `offer apply --publish`.
94
+ 2. **Allowing production and the Engage account** are deployment configuration: they need a
95
+ platform operator (an agent never sets them).
96
+ 3. **Sweep the dry runs that finished while the deployment was dry** — this needs a platform
97
+ operator too. A run opens its task only when it finishes, so a clean dry run from before
98
+ the switch would stay „Fertig" and never be transferred; the sweep (idempotent) opens
99
+ those tasks and reports `sandboxPinned`: the clean dry runs whose PINNED version still
100
+ targets the Sandbox school. They get no task (a live push would land in the Sandbox and
101
+ read as done) — re-pin each onto a `production` version (identical topology) or transfer
102
+ it by hand. The same rule refuses „Jetzt live übertragen" on such a run and opens no task
103
+ for it as soon as production is allowed (without that, a Sandbox live test stays
104
+ possible). Applications that ended without a place (rejected, withdrawn, waitlisted,
105
+ expired) are left out, live refuses them, and their open Engage task closes with them.
106
+ Every opened task „Engage-Übertragung prüfen" ends with „Jetzt live übertragen" (or with a
107
+ fixed answer + „Probelauf wiederholen" when the dry run lists issues). Test applications
108
+ are left out — they never go live.
109
+
110
+ ## Gotchas
111
+ - **Engage has no idempotency key.** Never re-run a push by hand: „Erneut übertragen"
112
+ resumes at the first open step and first asks Engage whether an unclear create landed.
113
+ An id the team types in is read back by id before it is taken.
114
+ - A create answers with the new id (`{"newPupilID":"4068"}`, `newContactID`, `newAddressID` —
115
+ observed in the sandbox write test); it is read back by id before it is taken. Without one, the id
116
+ comes from the difference of the list, verified the same way — never from a name search.
117
+ - A write without a body (`LinkAddressToPupil`) is sent with an EMPTY body: Engage answers
118
+ `411 Length Required` to a body-less PUT.
119
+ - A sibling already at the school holds the contact steps for the team: link the
120
+ existing Engage contact id or create a new contact.
121
+ - The API cannot enrol: the pupil lands as a prospective pupil at the configured
122
+ admission stage. Whether Engage enrols from its completion stage is open.
123
+ - A fee billed separately (`settlement: separate`) is never sent as „paid".
124
+ - **A dry run that waits for the team opens its task „Engage-Übertragung prüfen"**: when
125
+ it lists an issue (fix the answer or the mapping, then „Probelauf wiederholen"), or on a live
126
+ deployment when nothing stands between it and „Jetzt live übertragen". A clean re-run on a dry
127
+ deployment or „Jetzt live übertragen" closes it; a stop of the live run opens the live row's
128
+ own task. A gap that lies in the PINNED mapping (a value without an Engage id) keeps the task
129
+ open until the application is re-pinned or the answer changes.
130
+ - **The mapping an application uses is the one of its PINNED version.** A mapping fix reaches
131
+ applications started after the publish; older ones keep theirs (re-pin only with an identical
132
+ topology).
133
+ - **Address lines for address format `CL1-117`:** line 1 street + number (`subdwelling`), line 3
134
+ town (`street`), line 6 postcode (`postcode`). Line 5 feeds `town` — a postcode there is lost.
135
+ - **A lookup table is keyed by the exact value its source delivers — check each source's
136
+ vocabulary, `offer validate` does not.** The address field's `land` is its own closed
137
+ vocabulary in upper case (`DE`/`AT`/`CH`/`OTHER`), not the
138
+ lower-case ISO keys of a `countries` list. One `country` table for both made every
139
+ address „unmapped" (`[engagePush/unmappedLookup] … „DE"`), seen first in a production
140
+ dry run. Give such a source its own table (`addressCountry`). Leave a value
141
+ like `OTHER` unmapped: it names no country, and the push reports it instead of guessing.
142
+ Before going live, a dry run of an application with realistic data must list no issue
143
+ („Jetzt live übertragen" is hidden while any issue is listed).
144
+
145
+ ## Related
146
+ - [Name what a node reads](./addresses.md) · [Edit a live tenant's offer](./edit-live-tenant-offer.md)
147
+ - `plexus-cli offer workflow node add --help` (`--engage-*` flags) · `plexus-cli offer workflow node set-engage --help`
@@ -0,0 +1,67 @@
1
+ # Insert a node on one edge and rewire it
2
+
3
+ **When to use:** a new step must sit between two existing ones — e.g. „Unterlagen prüfen"
4
+ between the form and the first decision — and it must land on an existing board column
5
+ instead of adding one. Replaces the hand-written bundle edits of `apply-doccheck.py`
6
+ (insert a `review` node, re-point `form.next.standard`, add a progress mapping).
7
+
8
+ ## Prerequisites
9
+ - An offer bundle on disk (`offer export --slug <slug> --out <dir>`). Fully offline: every
10
+ command runs the publish gate on the result and writes nothing when it fails.
11
+ - The ids of the edge you cut: `plexus-cli offer workflow node list --dir <dir>` shows each
12
+ node's outcomes and targets.
13
+ - The state ids of the process board: `plexus-cli offer workflow show --dir <dir>`.
14
+
15
+ ## Steps
16
+ ```bash
17
+ # A review step on exactly the edge form —standard→ (whatever it pointed at):
18
+ plexus-cli offer workflow node add --dir <dir> --type review --id docCheck \
19
+ --label "Unterlagen prüfen" --label-en "Check documents" \
20
+ --reads '$node.form.admission.kind.masernschutz,$node.form.admission.kind.proofOfIdentity' \
21
+ --on-edge form:standard \
22
+ --progress reviewing=eingereicht \
23
+ --assign-decision bewerbungsteam
24
+
25
+ # Any other type works the same way, e.g. a waiting list on ONE decision outcome:
26
+ plexus-cli offer workflow node add --dir <dir> --type waitlist --id warteliste \
27
+ --label "Warteliste" --label-en "Waiting list" --on-edge decision:waitlisted
28
+
29
+ # Re-route an existing node's outcome afterwards:
30
+ plexus-cli offer workflow node set --dir <dir> --id docCheck --outcomes reviewed:decision
31
+ ```
32
+
33
+ - `--on-edge <from>:<outcome>` re-points exactly that edge to the new node; the new node's
34
+ outcomes continue to the edge's old target (route others with `--outcomes`). A sibling
35
+ outcome of `<from>` that reaches the same target keeps its route — unlike `--after`, which
36
+ rewires every edge of the anchor that points at its first target. The two exclude each other.
37
+ - `--progress <event>=<stateId>` puts the node's progress event on a state the board already
38
+ has (the family keeps reading „Eingereicht"). Without it the node gets its own state and
39
+ column — for a review that is a second „In Prüfung" column, which the board's
40
+ distinct-label rule (`checkProgressBoardDistinctLabels`) refuses.
41
+ - `--reads` names the fields a reviewer of a `--type review` ticks (entered, non-repeated
42
+ fields only), each by its full address `$node.<formNode>.admission.<field>` (single
43
+ quotes; a bare key is refused with the addresses visible at the new node). Without it the
44
+ review ticks nothing and still parks. Other node types declare reads with
45
+ `node set-reads` (which also takes the short form `--from <formNode> --field <path>,…`).
46
+ The form must run on every path before the new node (`[address/notVisible]` otherwise).
47
+
48
+ ## Verify
49
+ ```bash
50
+ plexus-cli offer validate --dir <dir>
51
+ plexus-cli offer workflow node list --dir <dir>
52
+ plexus-cli --env staging --tenant <t> offer plan --dir <dir> # expect: ~ workflow (+ ~ smartform if fields changed)
53
+ ```
54
+
55
+ ## Idempotency / gotchas
56
+ - Not idempotent: a second `node add` with the same `--id` is refused (`already exists`).
57
+ - An event the node type does not emit, or a state the board does not have, is refused by
58
+ name (`emits no progress event …`, `no state … on the process board`).
59
+ - A review holds every non-rejecting outcome until all `reads` fields are ticked — so a
60
+ waiting-list exit belongs on the decision after it, not on the review.
61
+ - Making the read fields required is a form edit: `offer form field set --key <k> --required`.
62
+ - Nothing here touches a live process until `offer apply --publish`; running applications
63
+ stay on their pinned version.
64
+
65
+ ## Related
66
+ - [Add a workflow node](./add-workflow-node.md) · [Edit a workflow node](./edit-workflow-node.md)
67
+ - [Name what a node reads](./addresses.md) · [Declare what a document node reads](./declare-node-reads.md) · [Edit a live tenant's offer](./edit-live-tenant-offer.md)
@@ -0,0 +1,162 @@
1
+ # Author a workflow node's tasks
2
+
3
+ **When to use:** give a workflow node a task a team must do — with a typed result
4
+ („Empfehlung", „Auswahl", Freitext, a priced item or a reduction), a blocking edge onto the same or a later node, a
5
+ spawn condition, evidence fields or its plain-text description — or change / remove one, without
6
+ hand-editing `workflow.json`. Works on any bundle: the `test` template, a fixture, or a
7
+ live tenant's export (then follow [Edit a live tenant's offer](./edit-live-tenant-offer.md)
8
+ around it).
9
+
10
+ ## The model in four lines
11
+ - A task (`node.tasks[]`, key unique per node) spawns when its node is entered — with
12
+ `--when`, only while that **boolean form export** holds. Everything a task reads names
13
+ its producer by full address (single quotes): `--when` / `--reads`
14
+ (`'$node.<formNode>.admission.<field>'`, one form per task — `[address/mixedForms]`),
15
+ `--documents` (`artifact:<own slot>`, `field:<address>`, `document:$node.<node>.document`),
16
+ `--family-request-fields <addresses>` / `--family-request-from '$node.<formNode>.admission'`,
17
+ a record field's `"suggest":{"kind":"form","address":"$node.…"}`. A bare key is refused
18
+ with the addresses visible at the node; the producer must run on every path before it
19
+ (`[address/notVisible]`). A value a task records is `$task.<taskKey>.<storeKey>` for its
20
+ readers — renaming the task key changes that address.
21
+ - `--blocks <templateKey>` withholds a sibling's actions on the same node;
22
+ `--blocks <nodeId>:<templateKey>` withholds a template on a **later** node (a
23
+ cross-node blocker: it survives its node and needs a `--result`). Intrinsic targets:
24
+ `decision`, `prepareDocument`, `releaseApproval`. The target's **whole** action group
25
+ waits — on a decision Freigeben, Warteliste, Ablehnen, and Nachrücken on the waitlist
26
+ it routes into. There is no partial hold; the staff escape is „Bewerbung
27
+ ablehnen".
28
+ - A task with `--result` closes only by recording it; `--chat` posts a result with text
29
+ into `all`, `team:<group>` or an authored channel key — never `external`.
30
+ - A result **with a value** (`choice`, `pricedItem`, `reduction`) writes it under the
31
+ task's own address — `--writes '$task.<thisTaskKey>.<storeKey>'`, stored as the bare
32
+ `result.writes` — and names it with `--result-label` (`--result-label-en`). A
33
+ `pricedItem` names its article with `--item <catalog key>` (not an address: the key of
34
+ the billing item); a `reduction` the derived price it lowers with
35
+ `--prices '$derived.<key>'`.
36
+ - The CLI checks **shape**; the graph rules (`[workItems/*]`, `[taskResult/*]`) are the
37
+ publish validators, run before every write and by `offer validate`.
38
+
39
+ ## Steps
40
+
41
+ ```bash
42
+ B=<bundle dir>
43
+
44
+ # What a node carries today.
45
+ plexus-cli offer workflow node list --dir $B
46
+ jq '.graph.nodes[] | {id, tasks}' $B/workflow.json # the tasks themselves
47
+
48
+ # 1. A cross-node blocker with a recommendation, posted to the whole team chat:
49
+ # the review team's estimate holds the whole Aufnahme decision.
50
+ plexus-cli offer workflow node task add --dir $B --node reviewEingereicht --key einschaetzung \
51
+ --title "Einschätzung abgeben" --title-en "Give an assessment" \
52
+ --team oeffentlichkeitsarbeit --due-in-days 3 \
53
+ --blocks aufnahme:decision \
54
+ --result recommendation --chat all
55
+
56
+ # 2. A conditional choice task that holds the contract (the reduced-fee shape):
57
+ # it exists only while the family asked for a reduction, comes back when the
58
+ # answer returns to „Ja", and is judged again when the income proofs change.
59
+ plexus-cli offer workflow node task add --dir $B --node contract --key ermaessigung \
60
+ --title "Ermäßigung prüfen" --team finanzen --due-in-days 5 \
61
+ --blocks prepareDocument \
62
+ --result choice --option voll="Voller Beitrag|Full fee" --option halb="Halbiert|Halved" \
63
+ --text-required --chat team:finanzen \
64
+ --when '$node.form.admission.ermaessigungBeantragt' --on-condition-return respawn \
65
+ --reads '$node.form.admission.gebuehren.einkommensnachweise' \
66
+ --description "Nachweise prüfen und die Ermäßigung berechnen." \
67
+ --description-en "Check the proofs and calculate the reduction."
68
+
69
+ # 3. A reduction that lowers the derived tuition (the `test` template's shape): it
70
+ # writes its deduction under its own address and names the price it lowers.
71
+ # The derived price must be declared in graph.processStore with
72
+ # derive.reduction = $task.reducedFee.reducedFee ([process/reductionPrices]).
73
+ plexus-cli offer workflow node task add --dir $B --node reviewEingereicht --key reducedFee \
74
+ --title "Ermäßigung berechnen" --team finanzen \
75
+ --result reduction --writes '$task.reducedFee.reducedFee' --prices '$derived.schulgeld' \
76
+ --result-label "Einkommensermäßigung (Abzug)" --result-label-en "Income-based reduction (deduction)" \
77
+ --chat team:finanzen
78
+ # A priced item names its article instead:
79
+ # --result pricedItem --item tuition --writes '$task.<key>.<storeKey>' --result-label "Schulgeld"
80
+
81
+ # 4. Change only what you name (everything else stays).
82
+ plexus-cli offer workflow node task set --dir $B --node reviewEingereicht --key einschaetzung --text-required
83
+ plexus-cli offer workflow node task set --dir $B --node contract --key ermaessigung --clear-description
84
+
85
+ # 5. Remove a task (refused while another task's --blocks still names it — the
86
+ # refusal lists the dependents; retarget or --clear-blocks them first).
87
+ plexus-cli offer workflow node task rm --dir $B --node contract --key ermaessigung
88
+
89
+ # 6. Gate, diff, release.
90
+ plexus-cli offer validate --dir $B
91
+ plexus-cli --env <env> offer plan --tenant <t> --dir $B
92
+ plexus-cli --env <env> offer apply --tenant <t> --dir $B --publish
93
+ ```
94
+
95
+ Every verb prints a field diff (`before` / `after`); `--json` gives
96
+ `{action, node, key, before, after, changes, dropped}`.
97
+
98
+ ## Example: move a task to another node on a live tenant
99
+
100
+ A form task `oeaEinschaetzung` on `formAnmeldung` should become one recommendation task
101
+ on the review node that holds the Aufnahme decision (reject / waitlist stay open).
102
+ Work in a scratch dir and keep an untouched copy to diff against:
103
+
104
+ ```bash
105
+ WORK=<scratch dir>/$(date +%F)-einschaetzung
106
+ T=<slug>; S=<offer-slug>
107
+ plexus-cli --env prod offer export --tenant $T --slug $S --out $WORK/$S
108
+ cp -a $WORK/$S $WORK/$S.orig
109
+
110
+ plexus-cli offer workflow node task rm --dir $WORK/$S --node formAnmeldung --key oeaEinschaetzung
111
+ plexus-cli offer workflow node task add --dir $WORK/$S --node reviewEingereicht --key einschaetzung \
112
+ --title "Einschätzung abgeben" --team oeffentlichkeitsarbeit --due-in-days 3 \
113
+ --blocks aufnahme:decision \
114
+ --result recommendation --chat all
115
+
116
+ diff -ru $WORK/$S.orig $WORK/$S # exactly these two tasks
117
+ plexus-cli offer validate --dir $WORK/$S
118
+ plexus-cli --env prod offer plan --tenant $T --dir $WORK/$S
119
+ plexus-cli --env prod offer apply --allow-prod --tenant $T --dir $WORK/$S --publish
120
+ ```
121
+
122
+ If `offer plan` refuses task fields that `offer validate` accepts, the deployment runs an
123
+ older release than your CLI — that needs a platform operator, not a bundle edit.
124
+
125
+ ## Verify
126
+ - `offer validate --dir $B` → clean.
127
+ - After publish, a new application reaching the node shows the task in the cockpit's
128
+ task rail („Empfehlung erfassen"), and the blocked target's actions stay held until
129
+ the result is recorded.
130
+
131
+ ## Gotchas
132
+ - **Only errors the edit introduces block the write.** Errors the bundle already carried
133
+ print as `⚠ vorbestehend …` and do not stop you — but `offer apply --publish` still
134
+ refuses them.
135
+ - **`--title` without `--title-en` writes the German into both locales**; `--title-en`
136
+ alone relabels English only.
137
+ - **`blocks.except` is retired** — `offer validate` refuses a bundle that still
138
+ carries it (`[workItems/blocksExceptRetired]`); re-setting `--blocks` drops it
139
+ (`⚠ blocks.except entfällt`). A result kind change drops
140
+ options / `textRequired` / `writes` / `label` / `item` / `prices` that no longer fit
141
+ (each listed under `dropped`), and keeps `chat`.
142
+ - **`--writes` takes this task's own address only** — `'$task.<key>.<storeKey>'` without a
143
+ path; a bare key, another task's address or a path is refused. `--prices` takes
144
+ `'$derived.<key>'`. Whether the store key is declared and the reduction's
145
+ `derive.reduction` points back at this task is the publish gate's check
146
+ (`[process/*]`).
147
+ - **`--option` replaces the whole list** — repeat every entry you keep.
148
+ - **`--description` is plain text under the title, never a checklist.** There are no
149
+ checklist steps any more (a bundle still carrying `steps` is refused at publish,
150
+ `[workItems/unknownKey]`, like any key a task does not declare); several work steps are several tasks (a `spawnTasks`
151
+ optional action, chained by `blocks`). Without `--description-en` the German stands in
152
+ for English.
153
+ - **An old bundle** (bare keys in `when` / `reads` / documents /
154
+ `familyRequest` / `suggest`) is refused (`[address/bareIdentifier]`) — export it again.
155
+ - **Offline, team and channel existence are not checked** — the tenant's permission
156
+ groups are known only at `offer apply --publish`.
157
+ - **The positional `node add|set --task`** replaces a node's whole task list and knows
158
+ none of the fields above; use it only to scaffold a new node.
159
+
160
+ ## Related
161
+ - [Name what a node reads](./addresses.md) · [Edit a workflow node](./edit-workflow-node.md) · [Author default chat channels](./workflow-default-channels.md)
162
+ - `plexus-cli offer workflow node task add --help` (every result kind and flag)
@@ -0,0 +1,65 @@
1
+ # Ask for an optional assessment („Einschätzung einholen")
2
+
3
+ **Use it to** model assessments the decision does not have to wait for: the
4
+ Öffentlichkeitsarbeit may say „Keine Einschätzung abgeben", the Schulleitung attaches a
5
+ Beurteilung as a file, and the Sekretariat can ask the Geschäftsführung for one more
6
+ assessment from the „Weiteres" menu — without a letter to the family.
7
+
8
+ ## The model
9
+
10
+ - **Optional task** (`optional: { skipLabel: { de, en } }`): nothing waits for it. The
11
+ task holds no step, so publish refuses it together with `blocks`, `gatesNode`,
12
+ `locksEvidence` and `completableIf` (`[workItems/optionalHolds]`). Its holder closes it
13
+ with its result or with the skip button. The Sekretariat skips it by deciding: an open
14
+ optional task then reads „Aufgabe hinfällig" in the Verlauf, naming the decider.
15
+ The result must be prose: free text, a recommendation, or a choice without `writes`
16
+ (`[workItems/optionalNeedsProseResult]`). A task of an action listed in `requiresOneOf`
17
+ is never optional (`[workItems/optionalInRequiresOneOf]`).
18
+ - **Files** (`result.files: true`): the person attaches up to 20 files next to the text.
19
+ Text and files go into the result's chat as one message. Find them later in the cockpit
20
+ under Verlauf/Aktivitäten (tab of the chat the result names), shown only to people who
21
+ read that chat — never under „Dokumente"
22
+ ([task-result-files.md](./task-result-files.md)).
23
+ - **„Einschätzung einholen"** is a `spawnTasks` optional action that references an optional
24
+ task assigned to a role (`--team geschaeftsfuehrung`). Pressing the action spawns that
25
+ task. A triggered optional task does not hold the decision. An internal task never
26
+ writes to the family.
27
+
28
+ ## CLI
29
+
30
+ Use a scratch dir for a real tenant
31
+ ([edit-live-tenant-offer.md](./edit-live-tenant-offer.md)).
32
+
33
+ ```bash
34
+ # 1. The ÖA assessment: optional, no longer blocking the decision
35
+ plexus-cli offer workflow node task set --dir <dir> --node aufnahme --key oeaEinschaetzung \
36
+ --clear-blocks --optional "Keine Einschätzung abgeben" --optional-en "No assessment"
37
+
38
+ # 2. The Schulleitung's assessment takes files (it may stay owed — keep its blocks)
39
+ plexus-cli offer workflow node task set --dir <dir> --node aufnahme --key schulleitungPruefung --files
40
+
41
+ # 3. The task the action spawns, then the action
42
+ plexus-cli offer workflow node task add --dir <dir> --node aufnahme --key gfEinschaetzung \
43
+ --title "Einschätzung Geschäftsführung" --title-en "Management assessment" \
44
+ --team geschaeftsfuehrung --due-in-days 3 --result freetext --chat all --files \
45
+ --optional "Keine Einschätzung abgeben" --optional-en "No assessment"
46
+ plexus-cli offer workflow node optional-action set --dir <dir> --node aufnahme --key einschaetzung \
47
+ --label "Einschätzung einholen" --label-en "Request an assessment" \
48
+ --tasks gfEinschaetzung --trigger bewerbungsteam --repeatable
49
+
50
+ plexus-cli offer validate --dir <dir>
51
+ ```
52
+
53
+ The group named by `--team` must hold `aufgaben.*` rights, or its members cannot open the
54
+ task (`tenant_admin` usually holds none). A rights failure blocks nothing at publish; it
55
+ only shows up in the cockpit.
56
+
57
+ ## Verify
58
+
59
+ 1. Cockpit, application on `aufnahme`: the decision row shows „Wartet auf: …" for
60
+ every task that still blocks it, but not for optional ones.
61
+ 2. As the ÖA member, open the task. The dialog shows „Keine Einschätzung abgeben" next to
62
+ „Ergebnis erfassen". After a skip, the Verlauf card reads „Keine Einschätzung abgeben".
63
+ 3. As the Sekretariat, use „Weiteres → Einschätzung einholen". The Geschäftsführung's queue
64
+ gets the task, and the family's Verlauf gets nothing.
65
+ 4. Testmodus: „<Aufgabe> (Test): Keine Einschätzung abgeben" skips through the real verb.