@waron97/prbot 3.2.0 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -194,7 +194,7 @@ prbot update
194
194
 
195
195
  ## agrippa
196
196
 
197
- Syncs Odoo workflow phase Python code and MFA records between the local filesystem and the RIP API. Tracks changes via checksums and detects conflicts before overwriting.
197
+ Syncs Odoo workflow phase Python code, MFA records, process-builder wizards, and long-running processes (LRPs) between the local filesystem and the RIP / Process Builder / Symphony APIs. Tracks changes via checksums and detects conflicts before overwriting.
198
198
 
199
199
  Credentials are inherited from the global prbot config (`~/.config/prbot/config`). Override per-workspace in `agrippa.yaml`.
200
200
 
@@ -208,9 +208,9 @@ agrippa init
208
208
 
209
209
  ### `agrippa clone`
210
210
 
211
- Clones all `from_code` phases for a selected workflow, a single MFA, or a **process-builder wizard**, into the workspace. Writes files to disk and registers them in `agrippa.yaml`. With no flag, prompts for the object type (MFA / Phase / Process Builder).
211
+ Clones all `from_code` phases for a selected workflow, a single MFA, a **process-builder wizard**, or a **long-running process (LRP)**, into the workspace. Writes files to disk and registers them in `agrippa.yaml`. With no flag, prompts for the object type (MFA / Phase / Process Builder / Long Running Process).
212
212
 
213
- A wizard is downloaded and **decomposed** into editable files (`structure.yaml`, `process.yaml`, `scripts/`, `pages/`, manifest); see [`agrippa pb`](#agrippa-pb) and [`agrippa-pb.md`](agrippa-pb.md).
213
+ A wizard or LRP is downloaded and **decomposed** into editable files (`structure.yaml`, `process.yaml`, `scripts/`, `pages/`, manifest) — LRPs share the same layout minus `pages/` (LRPs have no user tasks); see [`agrippa pb`](#agrippa-pb) and [`agrippa-pb.md`](agrippa-pb.md).
214
214
 
215
215
  ```bash
216
216
  agrippa clone
@@ -219,18 +219,21 @@ agrippa clone --mfa
219
219
  agrippa clone --phase --id 123 --path my-workflow/
220
220
  agrippa clone --pb # select a wizard
221
221
  agrippa clone --pb --name ml_review_billing --path my-wizard/
222
+ agrippa clone --lrp # live search LRPs by name
223
+ agrippa clone --lrp --name B2WA_ml_IFS_passive_trigger --path my-lrp/
222
224
  ```
223
225
 
224
226
  Options:
225
227
 
226
- | Flag | Description |
227
- | ---------------------- | ------------------------------------------------------------- |
228
- | `--phase` | Clone a phase (select a workflow) |
229
- | `--mfa` | Clone an MFA record |
230
- | `--pb` | Clone a process-builder wizard |
231
- | `--id <id>` | Skip selection, clone by ID (phase/mfa) |
232
- | `--name <document_id>` | Skip selection, clone a wizard by `document_id` (with `--pb`) |
233
- | `--path <path>` | Destination path (base dir for phases/wizard, file for MFA) |
228
+ | Flag | Description |
229
+ | ----------------- | --------------------------------------------------------------------------- |
230
+ | `--phase` | Clone a phase (select a workflow) |
231
+ | `--mfa` | Clone an MFA record |
232
+ | `--pb` | Clone a process-builder wizard |
233
+ | `--lrp` | Clone a long-running process |
234
+ | `--id <id>` | Skip selection, clone by ID (phase/mfa) |
235
+ | `--name <name>` | Skip selection: `document_id` (with `--pb`) or process name (with `--lrp`) |
236
+ | `--path <path>` | Destination path (base dir for phases/wizard/lrp, file for MFA) |
234
237
 
235
238
  ### `agrippa pull`
236
239
 
@@ -238,7 +241,7 @@ Fetches remote code for all tracked entries and shows what changed. Classifies e
238
241
 
239
242
  After pulling, also checks tracked workflows for newly added `from_code` phases and auto-clones any not yet present locally.
240
243
 
241
- Tracked **process-builder wizards** are also refreshed from upstream: the local project is re-decomposed from the latest payload (orphan script/page files pruned), with the same `fast-forward`/`conflict` classification (based on the wizard's upstream `updated_date` vs. the last pulled state). The current local state is backed up to `.backup/<timestamp>/<path>/local.json` first.
244
+ Tracked **process-builder wizards** and **long-running processes** are also refreshed from upstream: the local project is re-decomposed from the latest payload (orphan script files pruned; PB wizards also prune orphan pages), with the same `fast-forward`/`conflict` classification (PB: upstream `updated_date`; LRP: re-resolved-by-name payload checksum vs. the last pulled state — LRPs have no stable id, so every pull re-resolves the current id/tenantId by name first). The current local state is backed up to `.backup/<timestamp>/<path>/local.json` first.
242
245
 
243
246
  ```bash
244
247
  agrippa pull
@@ -246,14 +249,14 @@ agrippa pull
246
249
 
247
250
  ### `agrippa push`
248
251
 
249
- Pushes local file changes back to RIP (phases/MFAs) and to the Process Builder API (wizards). Backs up current remote state to `.backup/<timestamp>/` before overwriting. Same conflict detection as pull, with the concern inverted.
252
+ Pushes local file changes back to RIP (phases/MFAs), the Process Builder API (wizards), and Symphony (long-running processes). Backs up current remote state to `.backup/<timestamp>/` before overwriting. Same conflict detection as pull, with the concern inverted. LRP entries are located by **name**, not id (the Symphony id changes on every save), re-resolving the current id/tenantId immediately before saving.
250
253
 
251
- Pushing a wizard saves it as a **draft**; publish it so live consumers see the change with `--publish` (auto) or answer the prompt. Page edits (`pages/`) are saved independently of the whole-wizard save, mirroring the UI.
254
+ Pushing a wizard saves it as a **draft**; publish it so live consumers see the change with `--publish` (auto) or answer the prompt. Page edits (`pages/`) are saved independently of the whole-wizard save, mirroring the UI. Pushing an LRP saves the BPMN via Symphony's `tabulator` PATCH; **deploy** it (the LRP analog of publish) the same way — `--publish`/`--skip-publish` double as auto-deploy/never-deploy for LRPs too.
252
255
 
253
256
  ```bash
254
- agrippa push # prompts whether to publish each pushed wizard
255
- agrippa push --publish # auto-publish pushed wizards
256
- agrippa push --skip-publish # never publish (no prompt)
257
+ agrippa push # prompts whether to publish/deploy each pushed wizard/LRP
258
+ agrippa push --publish # auto-publish wizards, auto-deploy LRPs
259
+ agrippa push --skip-publish # never publish/deploy (no prompt)
257
260
  ```
258
261
 
259
262
  ### `agrippa diff [path]`
@@ -286,7 +289,7 @@ agrippa repair
286
289
 
287
290
  ### `agrippa pb`
288
291
 
289
- Local editing helpers for a **cloned process-builder wizard** (no network — they edit the decomposed files in place). Each operates on **one** wizard, resolved by `--pb <document_id>`, single-entry auto-select, or a fuzzy prompt.
292
+ Local editing helpers for a **cloned process-builder wizard or long-running process** (no network — they edit the decomposed files in place; both share the same decomposed layout and BPMN engine). Each operates on **one** wizard/LRP, resolved by `--pb <document_id_or_name>` (name for LRPs, since they have no `document_id`), single-entry auto-select, or a fuzzy prompt.
290
293
 
291
294
  These commands exist mainly so an **AI agent** can add/remove/connect blocks without hand-editing the multi-thousand-line `structure.yaml`. Human users typically edit blocks in the UI instead. Full agent-facing guide (also usable as a workspace `CLAUDE.md`): [`agrippa-pb.md`](agrippa-pb.md).
292
295
 
@@ -307,4 +310,9 @@ agrippa pb ls --pb ml_review_billing
307
310
  agrippa pb add --type scriptTask --name "Check pod" --pb ml_review_billing
308
311
  agrippa pb connect --from ScriptTask_x --to ExclusiveGateway_y --pb ml_review_billing
309
312
  agrippa pb format --pb ml_review_billing
313
+
314
+ agrippa pb ls --pb B2WA_ml_IFS_passive_trigger # LRP, resolved by name
315
+ agrippa pb add --type serviceTask --name "Call SAP" --pb B2WA_ml_IFS_passive_trigger
310
316
  ```
317
+
318
+ `userTask` (pages) can't be added to an LRP project — LRPs never have user tasks.
package/agrippa-pb.md CHANGED
@@ -1,14 +1,14 @@
1
- # Instructions for working with Phases, MFAs, and Process-Builder wizards
1
+ # Instructions for working with Phases, MFAs, Process-Builder wizards, and LRPs
2
2
 
3
- You are my coding assistant developing MFAs, phases, and process-builder wizards for
4
- our Odoo CRM. Follow instructions precisely, without doing fixes or enhancements
5
- outside the scope of your instructions.
3
+ You are my coding assistant developing MFAs, phases, process-builder wizards, and
4
+ long-running processes (LRPs) for our Odoo CRM. Follow instructions precisely, without
5
+ doing fixes or enhancements outside the scope of your instructions.
6
6
 
7
7
  This workspace is an **agrippa** workspace: a local checkout of Odoo code synced via
8
8
  the `agrippa` CLI. **You (the agent) never run `agrippa clone`, `agrippa pull`, or
9
9
  `agrippa push`** — those are human-only operations. You edit local files, and for
10
- wizards you also drive the `agrippa pb` editing commands (below). A human reviews and
11
- syncs.
10
+ wizards and LRPs you also drive the `agrippa pb` editing commands (below). A human
11
+ reviews and syncs.
12
12
 
13
13
  ---
14
14
 
@@ -57,35 +57,85 @@ match the result whose `next_phase` is X and use its `code_values`.
57
57
 
58
58
  ### Code inside phases and MFAs
59
59
 
60
- The following applies to both phases and MFAs.
61
-
62
- Some global utilities are made available inside the script, such as:
63
-
64
- - `json_dumps` (from `json.dumps`)
65
- - `json_loads` (from `json.loads`)
66
- - `datetime` (root-level import from datetime)
67
- - `dateutil` (root-level import from dateutil)
68
- - `request` (from `requests.request`)
69
- - `log` (logging method)
70
- - `format_exc` (from `traceback.format_exc`)
71
- - `first` (from `fields.first`)
72
- - `case_id` (phases onlythe current `helpdesk.ticket`)
73
-
74
- The value of the `result` variable is used to construct the HTTP response.
75
- The `make_response` helper allows returning error states, e.g.
76
- `result = make_response((500, 500), "Custom error message")`.
77
-
78
- Some patterns not allowed in Odoo's `safe_eval`:
79
-
80
- - f-strings
81
- - imports
82
- - lambdas inside functions (`def` inside another function is fine)
83
- - doc strings (you cannot write to dunder fields)
84
-
85
- Notably unavailable:
86
-
87
- - `getattr`
88
- - `setattr`
60
+ Phases and MFAs both run stored Python through Odoo 15 `safe_eval` (mode `exec`), but
61
+ they run in **different environments with different globals**. The shared sandbox rules
62
+ come first; the per-environment globals follow.
63
+
64
+ #### Shared sandbox rules (apply to both)
65
+
66
+ Patterns **not allowed** by `safe_eval`:
67
+
68
+ - **`import`** forbidden outright.
69
+ - **f-strings** — do not use them; use `"...{}".format(...)` instead. (The current
70
+ runtime's opcode whitelist technically permits them, but team convention and observed
71
+ failures mean you must avoid them.)
72
+ - **attribute assignment** (`obj.attr = x`) forbidden (`STORE_ATTR` is blocked). You
73
+ cannot set attributes; write through the ORM (`record.write({...})`) instead.
74
+ - **dunder names** any name containing `__` (e.g. `__class__`) is rejected. This is why
75
+ you cannot write doc strings.
76
+ - **`getattr` / `setattr` / `eval`** — not in the builtins, so unavailable.
77
+
78
+ Patterns that **are** allowed (but with a sharp limit):
79
+
80
+ - **lambdas and nested `def` are creatable, but cannot form closures.** A lambda/nested
81
+ function may reference only its **own parameters, module globals** (`env` etc.) **and
82
+ constants**. It must **not** capture a variable from the enclosing function the
83
+ closure-cell opcodes (`LOAD_DEREF`/`STORE_DEREF`/`LOAD_CLOSURE`/`MAKE_CELL`) are not
84
+ whitelisted, so a closure fails validation.
85
+ - ✅ works: `recs.filtered(lambda r: r.wizard_result == "CANCEL")` — no captured locals
86
+ - ❌ fails: `recs.filtered(lambda r: r.x == target)` — `target` is an enclosing local
87
+ - In practice nested functions are rarely usable, since they almost always close over
88
+ local state. Self-contained `.filtered(lambda r: ...)` predicates are the common
89
+ legitimate use and appear throughout real phase code.
90
+
91
+ #### Phase environment (`symple.triplet.phase`)
92
+
93
+ Phase code runs against a full Odoo "server action" context. Globals available:
94
+
95
+ - `env` — the **full Odoo ORM** (`env["model"].sudo().search/create/write/browse`)
96
+ - `case_id` — the current `helpdesk.ticket`
97
+ - `Command` — x2many command namespace
98
+ - `ValidationError` — raise to abort with a warning
99
+ - `request` (= `requests.request`), for outbound HTTP
100
+ - `json_dumps` (= `json.dumps`), `json_load` (= `json.load` — note: **not** `json_loads`)
101
+ - `time`, `datetime`, `dateutil`, `timezone`, `float_compare`, `OrderedDict`
102
+ - `b64encode`, `b64decode`
103
+ - `log(message, level='info')`, `format_exc` (= `traceback.format_exc`), `first`
104
+ (= `fields.first`), `uid`, `user`
105
+
106
+ There is **no** `make_response` in phases.
107
+
108
+ **Returning a result:** assign `result` as a **string** — it must match one of the phase's
109
+ `code_values` in `workflow.yml` (e.g. `result = "RES1"`). It is **not** a dict. The string
110
+ is matched against `result.code.configurator` to pick the outgoing edge.
111
+
112
+ **Reporting errors:** write the message onto the case, conventionally
113
+ `case_id.write({"info_message": "..."})` (`error_message` also exists but `info_message`
114
+ is the prevailing choice).
115
+
116
+ **Case helpers** (defined on `helpdesk.ticket` in `sorgenia_tools`):
117
+
118
+ - `case_id.kv_store()` — the per-case key/value store (see below).
119
+ - `case_id.last_staging("<process_name>")` — returns the latest
120
+ `symple.pb.process.data` for that process on the case, raising `ValidationError` if
121
+ none exists. Prefer this over searching `symple.pb.process.data` by hand.
122
+
123
+ #### MFA environment (RIP)
124
+
125
+ MFA code runs to build an HTTP response, with a much smaller context. Globals available:
126
+
127
+ - `env` — the Odoo ORM
128
+ - `make_response` — build a response, e.g.
129
+ `result = make_response((500, 500), "Custom error message")`
130
+ - `logger` — a standard Python logger
131
+ - `result` — assign the response payload (a recordset or JSON-serialisable content); it
132
+ is consumed to construct the HTTP response
133
+ - the incoming request data: `method`, `model`, `records`, `user`, `args`, `headers`,
134
+ `body`, `httprequest`
135
+ - `datetime`, `dateutil`
136
+
137
+ MFAs have **no** `case_id`, `Command`, `ValidationError`, `json_dumps`, `first`, `log`,
138
+ or `format_exc` by default.
89
139
 
90
140
  ### Communication between phases
91
141
 
@@ -108,26 +158,42 @@ records.
108
158
 
109
159
  ---
110
160
 
111
- ## Process-builder wizards
112
-
113
- A wizard is a BPMN process the CRM runs as a guided flow. Locally it has been
114
- **decomposed** into editable files. For wizards you are expected to **drive the
115
- `agrippa pb` commands** for structural changes they hide BPMN id generation, the
116
- nested-YAML graph, dangling-edge cleanup, and the script/page/manifest bookkeeping you
117
- would otherwise get wrong by hand.
161
+ ## Process-builder wizards and long-running processes (LRPs)
162
+
163
+ A wizard or LRP is a BPMN process the CRM runs as a guided flow (wizards) or a
164
+ long-running background process (LRPs). Both are the **same underlying Activiti/
165
+ Symphony BPMN XML**, so locally they share **one** decomposed layout and one `pb`
166
+ editing suite everything below applies to both unless a difference is called out.
167
+ For wizards/LRPs you are expected to **drive the `agrippa pb` commands** for
168
+ structural changes — they hide BPMN id generation, the nested-YAML graph,
169
+ dangling-edge cleanup, and the script/page/manifest bookkeeping you would otherwise
170
+ get wrong by hand.
171
+
172
+ **LRP-specific differences:**
173
+
174
+ - **No pages.** LRPs never contain `userTask`. `pb add --type userTask` errors out on
175
+ an LRP project.
176
+ - **Selector is `name`, not `document_id`.** LRPs have no stable id — the Symphony id
177
+ changes on every save — so `--pb <name>` (not a `document_id`) is how you target an
178
+ LRP project. `pb ls`/error messages show `name` when `document_id` is absent.
179
+ - **A few extra node types show up more often**: `intermediateCatchEvent`,
180
+ `intermediateThrowEvent`, `callActivity`, `parallelGateway`, `eventBasedGateway` (see
181
+ Node types below) — these exist for PBs too but are common in LRPs.
182
+ - Publishing is called **deploy** for LRPs (same human-only `agrippa push
183
+ --publish`/`--skip-publish` flow as wizard publish — not a `pb` command).
118
184
 
119
185
  > **You never run `agrippa push` or `agrippa pull`.** You only edit local files and run
120
- > the local, read/write `agrippa pb` subcommands. A human syncs and publishes.
186
+ > the local, read/write `agrippa pb` subcommands. A human syncs and publishes/deploys.
121
187
 
122
188
  ### Decomposed project layout
123
189
 
124
190
  ```
125
- <wizard-dir>/
191
+ <wizard-or-lrp-dir>/
126
192
  process.yaml identity + top-level flags (envelope)
127
193
  structure.yaml THE graph: nodes hold their outgoing `edges` and nested
128
194
  `nodes` (for subProcess/transaction); inline geometry
129
195
  scripts/NNNN_*.js one scriptTask body each (byte-exact)
130
- pages/<formKey>.yml one userTask page object each
196
+ pages/<formKey>.yml one userTask page object each (PB only — LRPs have no pages/)
131
197
  .agrippa-pb.json manifest: scalars, namespaces, id↔file maps (do not hand-edit)
132
198
  ```
133
199
 
@@ -149,17 +215,21 @@ A node looks like:
149
215
  ```
150
216
 
151
217
  Node types: `startEvent`, `endEvent`, `boundaryEvent`, `exclusiveGateway`,
152
- `scriptTask`, `serviceTask`, `userTask`, `subProcess`, `transaction`.
218
+ `scriptTask`, `serviceTask`, `userTask` (PB only), `subProcess`, `transaction`,
219
+ `intermediateCatchEvent`, `intermediateThrowEvent`, `callActivity`,
220
+ `parallelGateway`, `eventBasedGateway`.
153
221
 
154
222
  ### Your loop
155
223
 
156
224
  ```
157
225
  agrippa pb ls # discover node/edge ids
158
226
  agrippa pb add / rm / connect / disconnect / set-default # change the graph
227
+ agrippa pb lint # check for structural issues before handing off
159
228
  # STOP. Do NOT format. Do NOT push. Report back to the human (see Formatting below).
160
229
  ```
161
230
 
162
- Every `pb` command targets **one** wizard. **Always pass `--pb <document_id>`** so you
231
+ Every `pb` command targets **one** wizard or LRP. **Always pass
232
+ `--pb <document_id_or_name>`** (a wizard's `document_id`, or an LRP's `name`) so you
163
233
  never hit an interactive prompt.
164
234
 
165
235
  ### Commands
@@ -167,7 +237,7 @@ never hit an interactive prompt.
167
237
  #### `pb ls` — discover ids
168
238
 
169
239
  ```bash
170
- agrippa pb ls --pb <document_id>
240
+ agrippa pb ls --pb <document_id_or_name>
171
241
  ```
172
242
 
173
243
  Flat list of every node with its id, type, name, parent (if nested), and outgoing
@@ -186,17 +256,21 @@ ExclusiveGateway_1lghfov (exclusiveGateway) "eg err"
186
256
  #### `pb add` — add a node
187
257
 
188
258
  ```bash
189
- agrippa pb add --type scriptTask --name "Check pod" --pb <document_id>
190
- agrippa pb add --type userTask --name "Review" --pb <document_id>
191
- agrippa pb add --type subProcess --name "Retry loop" --pb <document_id>
192
- agrippa pb add --type scriptTask --name "Inner" --parent SubProcess_x --pb <document_id>
259
+ agrippa pb add --type scriptTask --name "Check pod" --pb <document_id_or_name>
260
+ agrippa pb add --type userTask --name "Review" --pb <document_id_or_name>
261
+ agrippa pb add --type subProcess --name "Retry loop" --pb <document_id_or_name>
262
+ agrippa pb add --type scriptTask --name "Inner" --parent SubProcess_x --pb <document_id_or_name>
193
263
  ```
194
264
 
195
265
  Prints the new node id. Side effects, handled for you:
196
266
 
197
267
  - `scriptTask` → creates an **empty** `scripts/NNNN_<slug>.js`; edit that file for the body.
198
268
  - `userTask` → creates a **stub** `pages/<slug>.yml` + a manifest entry (no page content).
269
+ **PB only** — rejected with an error on an LRP project (LRPs have no user tasks).
199
270
  - `subProcess`/`transaction` → empty container; add children with `--parent <its-id>`.
271
+ - new event/gateway types (`intermediateCatchEvent`, `intermediateThrowEvent`,
272
+ `callActivity`, `parallelGateway`, `eventBasedGateway`) → sized/laid out like their
273
+ closest existing counterpart, no extra scaffold files.
200
274
 
201
275
  The node is added **disconnected** with placeholder geometry. Connect it next.
202
276
 
@@ -204,7 +278,7 @@ The node is added **disconnected** with placeholder geometry. Connect it next.
204
278
  instead of adding it disconnected:
205
279
 
206
280
  ```bash
207
- agrippa pb add --type scriptTask --name "New" --from A --to End --pb <document_id>
281
+ agrippa pb add --type scriptTask --name "New" --from A --to End --pb <document_id_or_name>
208
282
  ```
209
283
 
210
284
  Requires **exactly one** existing edge `A → End` already (errors if there's none, or
@@ -216,7 +290,7 @@ new-node → `End`. `--parent` is implied by `A`/`End`'s container, so don't pas
216
290
  #### `pb rm` — remove a node
217
291
 
218
292
  ```bash
219
- agrippa pb rm --id ScriptTask_0mmmti4 --pb <document_id>
293
+ agrippa pb rm --id ScriptTask_0mmmti4 --pb <document_id_or_name>
220
294
  ```
221
295
 
222
296
  Removes the node (and, for a container, its children), **every edge pointing at it**
@@ -227,35 +301,58 @@ manifest entries. No dangling references left behind.
227
301
 
228
302
  ```bash
229
303
  # plain sequence
230
- agrippa pb connect --from ScriptTask_a --to ScriptTask_b --pb <document_id>
304
+ agrippa pb connect --from ScriptTask_a --to ScriptTask_b --pb <document_id_or_name>
231
305
 
232
- # gateway branch with a condition
306
+ # named branch (required when the source has 2+ outgoing flows and this is non-default)
233
307
  agrippa pb connect --from ExclusiveGateway_g --to ScriptTask_b \
234
- --condition '${isAlive}' --pb <document_id>
308
+ --name "alive" --condition '${isAlive}' --pb <document_id_or_name>
235
309
 
236
- # the gateway's fallback branch
310
+ # the gateway's fallback branch (default needs no name)
237
311
  agrippa pb connect --from ExclusiveGateway_g --to EndEvent_err \
238
- --default --pb <document_id>
312
+ --default --pb <document_id_or_name>
239
313
  ```
240
314
 
241
315
  A flow's id is printed. Conditions default to `xsi:type="tFormalExpression"`
242
- (override with `--condition-type`). **Gateway rule** (enforced by Activiti, warned by
243
- this command): an `exclusiveGateway` with more than one outgoing flow must have
244
- **exactly one** `--default` flow, and **every other** outgoing flow must carry a
245
- `--condition`. Heed the `!` warnings `connect` prints.
316
+ (override with `--condition-type`). **Rules** (warned by this command and by `pb lint`):
317
+
318
+ - An `exclusiveGateway` with more than one outgoing flow must have **exactly one**
319
+ `--default` flow, and **every other** outgoing flow must carry a `--condition`.
320
+ - Any node with more than one outgoing flow: every **non-default** flow must have a
321
+ `--name` (operators need labels to distinguish branches at runtime).
322
+ - Only `exclusiveGateway` may have more than one **incoming** flow.
323
+ - An `exclusiveGateway` may not have both multiple incoming **and** multiple outgoing
324
+ flows — pick one direction (merging or splitting), not both.
325
+
326
+ Heed the `!` warnings `connect` prints.
246
327
 
247
328
  #### `pb disconnect` — remove a flow
248
329
 
249
330
  ```bash
250
- agrippa pb disconnect --id SequenceFlow_1lso8x0 --pb <document_id>
251
- agrippa pb disconnect --from ScriptTask_a --to ScriptTask_b --pb <document_id>
331
+ agrippa pb disconnect --id SequenceFlow_1lso8x0 --pb <document_id_or_name>
332
+ agrippa pb disconnect --from ScriptTask_a --to ScriptTask_b --pb <document_id_or_name>
333
+ ```
334
+
335
+ #### `pb lint` — check for structural issues
336
+
337
+ ```bash
338
+ agrippa pb lint --pb <document_id_or_name>
252
339
  ```
253
340
 
341
+ Runs all diagram rules and prints any violations. Exits 1 if issues found. Run after
342
+ every batch of structural edits before handing off to a human. Rules checked:
343
+
344
+ - `exclusiveGateway` default/condition rule (2+ outgoing → one default, rest conditioned)
345
+ - Non-default outgoing flows must have a name when the source has 2+ outgoing flows
346
+ - Only `exclusiveGateway` may have multiple incoming flows
347
+ - `exclusiveGateway` may not have both multiple incoming and multiple outgoing flows
348
+
349
+ `pb format` and `pb connect` also surface the same warnings inline.
350
+
254
351
  #### `pb set-default` — change a gateway's default flow
255
352
 
256
353
  ```bash
257
- agrippa pb set-default --id SequenceFlow_b --pb <document_id>
258
- agrippa pb set-default --from ExclusiveGateway_g --to EndEvent_err --pb <document_id>
354
+ agrippa pb set-default --id SequenceFlow_b --pb <document_id_or_name>
355
+ agrippa pb set-default --from ExclusiveGateway_g --to EndEvent_err --pb <document_id_or_name>
259
356
  ```
260
357
 
261
358
  Flips an **already-existing** flow to be its source gateway's `default`, by edge id or
@@ -268,7 +365,7 @@ non-default flow left without one).
268
365
  #### `pb preview` — visual check
269
366
 
270
367
  ```bash
271
- agrippa pb preview --pb <document_id> --out /tmp/wizard.svg
368
+ agrippa pb preview --pb <document_id_or_name> --out /tmp/wizard.svg
272
369
  ```
273
370
 
274
371
  Renders the current geometry to an SVG so a human can eyeball the result. Safe to run.
@@ -276,8 +373,8 @@ Not byte-faithful to the real renderer — a sanity check only.
276
373
 
277
374
  ### Formatting — a human decision, do NOT run it yourself
278
375
 
279
- `agrippa pb format` re-lays-out the **entire** wizard with an automatic algorithm.
280
- On an existing wizard this **discards the human's hand-tuned layout** and produces a
376
+ `agrippa pb format` re-lays-out the **entire** wizard/LRP with an automatic algorithm.
377
+ On an existing project this **discards the human's hand-tuned layout** and produces a
281
378
  drastically different diagram. That may or may not be acceptable — **only the human
282
379
  decides.**
283
380
 
@@ -286,9 +383,9 @@ positions, and **you stop there**. Report to the human what you changed and that
286
383
  new blocks need positioning, then let them choose one of:
287
384
 
288
385
  1. **Run `agrippa pb format`** themselves — accepts a full automatic re-layout of the
289
- whole wizard (existing arrangement is lost), or
290
- 2. **Position the new blocks by hand in the UI** after a human pushes the change —
291
- preserving the existing layout.
386
+ whole project (existing arrangement is lost), or
387
+ 2. **Position the new blocks by hand in the UI** after a human pushes/deploys the
388
+ change — preserving the existing layout.
292
389
 
293
390
  Never run `pb format` unless the human explicitly tells you to, with that trade-off
294
391
  understood.
@@ -324,7 +421,8 @@ graph (`pb ls`) first if so.
324
421
 
325
422
  ```bash
326
423
  agrippa pb add --type exclusiveGateway --name "alive?" --pb W # → ExclusiveGateway_g
327
- agrippa pb connect --from ExclusiveGateway_g --to ScriptTask_ok --condition '${isAlive}' --pb W
424
+ agrippa pb connect --from ExclusiveGateway_g --to ScriptTask_ok \
425
+ --name "alive" --condition '${isAlive}' --pb W
328
426
  agrippa pb connect --from ExclusiveGateway_g --to EndEvent_err --default --pb W
329
427
  ```
330
428
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@waron97/prbot",
3
- "version": "3.2.0",
3
+ "version": "3.3.0",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -7,12 +7,16 @@ import { describeWorkflow, getPhasesByWorkflow, listMfas, listWorkflows } from '
7
7
  import { computeChecksum } from '../lib/checksum.js';
8
8
  import { loadEffectiveEnv, readConfig, writeConfig } from '../lib/config.js';
9
9
  import { defaultMfaPath, toSlug, writeCodeFile, writeWorkflowDoc } from '../lib/workspace.js';
10
+ import { cloneLrp } from './cloneLrp.js';
10
11
  import { clonePb } from './clonePb.js';
11
12
 
12
13
  async function clone(opts) {
13
14
  if (opts.pb) {
14
15
  return clonePb(opts);
15
16
  }
17
+ if (opts.lrp) {
18
+ return cloneLrp(opts);
19
+ }
16
20
 
17
21
  const config = readConfig();
18
22
  loadEffectiveEnv(config);
@@ -26,11 +30,13 @@ async function clone(opts) {
26
30
  { name: 'MFA', value: 'mfa' },
27
31
  { name: 'Phase', value: 'phase' },
28
32
  { name: 'Process Builder', value: 'pb' },
33
+ { name: 'Long Running Process', value: 'lrp' },
29
34
  ],
30
35
  });
31
36
  }
32
- // Process-builder wizards have their own clone flow (PB_URL, recompose verify).
37
+ // Process-builder wizards and LRPs have their own clone flow (recompose verify).
33
38
  if (objectType === 'pb') return clonePb(opts);
39
+ if (objectType === 'lrp') return cloneLrp(opts);
34
40
 
35
41
  const ripUrl = process.env.RIP_URL;
36
42
  if (!ripUrl)
@@ -0,0 +1,114 @@
1
+ import search from '@inquirer/search';
2
+ import inquirer from 'inquirer';
3
+ import { getToken } from '../../lib/auth.js';
4
+ import { computeChecksum } from '../lib/checksum.js';
5
+ import { loadEffectiveEnv, readConfig, writeConfig } from '../lib/config.js';
6
+ import { getLrpXml, listLrps, resolveLrpByName } from '../lib/lrpApi.js';
7
+ import { comparePayload, decompose, recompose, stableStringify } from '../lib/pbProject.js';
8
+ import { projectReader, writeProject } from '../lib/pbWorkspace.js';
9
+
10
+ // Clone a long-running process (LRP). Structurally identical to a PB clone
11
+ // (same decompose/recompose/checksum machinery — LRPs are plain Activiti BPMN
12
+ // too), but selection and identity are name-based: the tabulator `id` changes
13
+ // on every save/version bump (verified live — a re-fetched id differs from a
14
+ // previously captured one for the same process), so it can never be used as
15
+ // a stable workspace key the way PB's guid is.
16
+ async function cloneLrp(opts) {
17
+ const config = readConfig();
18
+ loadEffectiveEnv(config);
19
+
20
+ if (!process.env.IMPORTEXPORT_URL) {
21
+ throw new Error(
22
+ 'IMPORTEXPORT_URL is not configured. Run `prbot init` or set it in agrippa.yaml.'
23
+ );
24
+ }
25
+
26
+ const token = await getToken();
27
+
28
+ // Selection: by --name (exact) or interactive server-side search (LRPs
29
+ // are searched server-side by name — see listLrps — not fetched-all-then-
30
+ // fuzzy-filtered like PBs).
31
+ let chosen;
32
+ if (opts.name) {
33
+ chosen = await resolveLrpByName(token, opts.name);
34
+ } else {
35
+ console.log('Fetching long-running process list...');
36
+ const initial = await listLrps(token, null);
37
+ let controller = null;
38
+ chosen = await search({
39
+ message: 'Select a long-running process:',
40
+ source: async (input) => {
41
+ if (!input) return initial.map((p) => ({ name: p.name, value: p }));
42
+ if (controller) controller.abort();
43
+ controller = new AbortController();
44
+ try {
45
+ const list = await listLrps(token, input, controller.signal);
46
+ return list.map((p) => ({ name: p.name, value: p }));
47
+ } catch {
48
+ return initial.map((p) => ({ name: p.name, value: p }));
49
+ }
50
+ },
51
+ });
52
+ }
53
+
54
+ // Destination directory.
55
+ let dest = opts.path ?? null;
56
+ if (!dest) {
57
+ const { inputPath } = await inquirer.prompt([
58
+ {
59
+ type: 'input',
60
+ name: 'inputPath',
61
+ message: 'Destination directory:',
62
+ default: chosen.name.replace(/^B2WA_/, ''),
63
+ },
64
+ ]);
65
+ dest = inputPath;
66
+ }
67
+
68
+ console.log(`Fetching "${chosen.name}"...`);
69
+ const { xml, description } = await getLrpXml(token, chosen.id);
70
+ const payload = { built_page: xml };
71
+
72
+ const { files } = decompose(payload);
73
+ writeProject(dest, files);
74
+
75
+ const scriptCount = Object.keys(files).filter((p) => p.startsWith('scripts/')).length;
76
+ console.log(`Cloned to ${dest}/ (${scriptCount} script(s)).`);
77
+
78
+ // Prove the clone reconstructs the original XML (0-loss bar A). LRPs have
79
+ // no `pages` at all — recompose always synthesizes `pages: []`, which
80
+ // comparePayload (shared with PB, where it's meaningful) would otherwise
81
+ // flag as noise on every single LRP clone.
82
+ const rebuilt = recompose(projectReader(dest));
83
+ const diffs = comparePayload(payload, rebuilt).filter((d) => !d.startsWith('pages:'));
84
+ if (diffs.length) {
85
+ console.warn('WARNING: round-trip verification found differences:');
86
+ diffs.forEach((d) => console.warn(' - ' + d));
87
+ } else {
88
+ console.log('Round-trip verified: recomposed payload is identical (0 information loss).');
89
+ }
90
+
91
+ // Register in the workspace for later pull/push — keyed by NAME, not id.
92
+ config.workspace = config.workspace || [];
93
+ const existing = config.workspace.findIndex(
94
+ (e) => e.object_type === 'long_running_process' && e.name === chosen.name
95
+ );
96
+ const entry = {
97
+ path: dest,
98
+ object_type: 'long_running_process',
99
+ name: chosen.name,
100
+ tenant_id: chosen.tenantId,
101
+ svg: chosen.bpmnFileSvg,
102
+ description,
103
+ // Baseline for push classification (see pull.js/push.js): checksum of
104
+ // the *recomposed* payload, changes only when local files change.
105
+ checksum_at_pull: computeChecksum(stableStringify(rebuilt)),
106
+ version: chosen.version,
107
+ status: chosen.status,
108
+ };
109
+ if (existing >= 0) config.workspace[existing] = entry;
110
+ else config.workspace.push(entry);
111
+ writeConfig(config);
112
+ }
113
+
114
+ export { cloneLrp };