@waron97/prbot 3.1.3 → 3.2.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.
Files changed (39) hide show
  1. package/README.md +50 -11
  2. package/agrippa-pb.md +350 -0
  3. package/package.json +7 -3
  4. package/src/agrippa/commands/clone.js +29 -13
  5. package/src/agrippa/commands/clonePb.js +107 -0
  6. package/src/agrippa/commands/diff.js +94 -18
  7. package/src/agrippa/commands/init.js +66 -12
  8. package/src/agrippa/commands/initPhase.js +16 -17
  9. package/src/agrippa/commands/pb.js +279 -0
  10. package/src/agrippa/commands/pull.js +119 -13
  11. package/src/agrippa/commands/pullPb.js +54 -0
  12. package/src/agrippa/commands/push.js +112 -47
  13. package/src/agrippa/commands/pushPb.js +87 -0
  14. package/src/agrippa/commands/repair.js +3 -1
  15. package/src/agrippa/index.js +138 -14
  16. package/src/agrippa/lib/api.js +17 -3
  17. package/src/agrippa/lib/checksum.js +3 -1
  18. package/src/agrippa/lib/config.js +2 -2
  19. package/src/agrippa/lib/pbApi.js +71 -0
  20. package/src/agrippa/lib/pbEdit.js +467 -0
  21. package/src/agrippa/lib/pbLayout.js +283 -0
  22. package/src/agrippa/lib/pbModel.js +698 -0
  23. package/src/agrippa/lib/pbPreview.js +151 -0
  24. package/src/agrippa/lib/pbProject.js +390 -0
  25. package/src/agrippa/lib/pbWorkspace.js +30 -0
  26. package/src/agrippa/lib/workspace.js +23 -3
  27. package/src/commands/autopr.js +5 -2
  28. package/src/commands/changelog.js +7 -2
  29. package/src/commands/export.js +3 -3
  30. package/src/commands/exportEmailTemplates.js +25 -15
  31. package/src/commands/exportImperex.js +4 -4
  32. package/src/commands/exportLrp.js +10 -7
  33. package/src/commands/exportPb.js +4 -5
  34. package/src/commands/exportWorkflow.js +27 -14
  35. package/src/commands/init.js +7 -0
  36. package/src/commands/routine.js +7 -5
  37. package/src/index.js +24 -7
  38. package/src/lib/premigrate.js +3 -3
  39. package/src/lib/updateCheck.js +5 -2
package/README.md CHANGED
@@ -5,7 +5,7 @@ CLI tool for managing PRs, changelogs, and Odoo workflow XML files in the addons
5
5
  ## Install
6
6
 
7
7
  ```bash
8
- npm install -g .
8
+ npm install -g @waron97/prbot
9
9
  ```
10
10
 
11
11
  ## Setup
@@ -36,7 +36,7 @@ Prompts for all required config values and writes them to `~/.config/prbot/confi
36
36
  | `DEVOPS_PROJECT` | Azure DevOps project |
37
37
  | `DEVOPS_REPO` | Azure DevOps repository name |
38
38
  | `AUTOPR_TARGET_BRANCH` | Target branch for auto-created PRs (default: `15.0-dev`) |
39
- | `IMPORTEXPORT_URL` | ImportExport API base URL |
39
+ | `IMPORTEXPORT_URL` | ImportExport API base URL |
40
40
 
41
41
  ## Commands
42
42
 
@@ -208,23 +208,29 @@ agrippa init
208
208
 
209
209
  ### `agrippa clone`
210
210
 
211
- Clones all `from_code` phases for a selected workflow, or a single MFA, into the workspace. Writes files to disk and registers them in `agrippa.yaml`.
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).
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).
212
214
 
213
215
  ```bash
214
216
  agrippa clone
215
217
  agrippa clone --phase
216
218
  agrippa clone --mfa
217
219
  agrippa clone --phase --id 123 --path my-workflow/
220
+ agrippa clone --pb # select a wizard
221
+ agrippa clone --pb --name ml_review_billing --path my-wizard/
218
222
  ```
219
223
 
220
224
  Options:
221
225
 
222
- | Flag | Description |
223
- | --------------- | -------------------------------------------------------- |
224
- | `--phase` | Clone a phase (select a workflow) |
225
- | `--mfa` | Clone an MFA record |
226
- | `--id <id>` | Skip selection, clone by ID |
227
- | `--path <path>` | Destination path (base dir for phases, file path for MFA)|
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
234
 
229
235
  ### `agrippa pull`
230
236
 
@@ -232,25 +238,34 @@ Fetches remote code for all tracked entries and shows what changed. Classifies e
232
238
 
233
239
  After pulling, also checks tracked workflows for newly added `from_code` phases and auto-clones any not yet present locally.
234
240
 
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.
242
+
235
243
  ```bash
236
244
  agrippa pull
237
245
  ```
238
246
 
239
247
  ### `agrippa push`
240
248
 
241
- Pushes local file changes back to RIP. Backs up current remote code to `.backup/<timestamp>/` before overwriting. Same conflict detection as pull, with the concern inverted.
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.
250
+
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.
242
252
 
243
253
  ```bash
244
- agrippa push
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)
245
257
  ```
246
258
 
247
259
  ### `agrippa diff [path]`
248
260
 
249
261
  Shows a diff between local files and remote code. Optionally filter to a specific file path.
250
262
 
263
+ For process-builder wizards, diffs the whole project tree against what the workspace would look like if the wizard were decomposed fresh from upstream right now (`.backup/` and `preview.svg` are excluded as local-only artifacts).
264
+
251
265
  ```bash
252
266
  agrippa diff
253
267
  agrippa diff my-workflow/some-phase.py
268
+ agrippa diff ml_review_billing # a cloned wizard's directory
254
269
  ```
255
270
 
256
271
  ### `agrippa init-phase`
@@ -269,3 +284,27 @@ Removes entries from `agrippa.yaml` whose local files no longer exist on disk.
269
284
  agrippa repair
270
285
  ```
271
286
 
287
+ ### `agrippa pb`
288
+
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.
290
+
291
+ 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
+
293
+ Newly added blocks get placeholder geometry. `pb format` re-lays-out the **whole** diagram (discarding any hand-tuned layout), so running it is a deliberate **human decision** — the alternative is positioning the new blocks by hand in the UI. Agents are expected to make structural edits but **not** to run `format` or to `pull`/`push`; a human reviews and syncs.
294
+
295
+ | Command | Purpose |
296
+ | --------------- | ------------------------------------------------------------------------------------ |
297
+ | `pb format` | Re-lay-out the **entire** diagram (elkjs, left→right); overwrites existing layout |
298
+ | `pb add` | Add a node (`--type`, `--name`, `--parent`); scaffolds script/page files |
299
+ | `pb rm` | Remove a node (`--id`), its edges, and its script/page files |
300
+ | `pb connect` | Add a flow (`--from`, `--to`, `--condition`, `--default`); enforces the gateway rule |
301
+ | `pb disconnect` | Remove a flow (`--id`, or `--from`/`--to`) |
302
+ | `pb ls` | List nodes and edges with their ids (discover targets without reading the YAML) |
303
+ | `pb preview` | Render the diagram to an SVG (`--out`) for a quick visual check |
304
+
305
+ ```bash
306
+ agrippa pb ls --pb ml_review_billing
307
+ agrippa pb add --type scriptTask --name "Check pod" --pb ml_review_billing
308
+ agrippa pb connect --from ScriptTask_x --to ExclusiveGateway_y --pb ml_review_billing
309
+ agrippa pb format --pb ml_review_billing
310
+ ```
package/agrippa-pb.md ADDED
@@ -0,0 +1,350 @@
1
+ # Instructions for working with Phases, MFAs, and Process-Builder wizards
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.
6
+
7
+ This workspace is an **agrippa** workspace: a local checkout of Odoo code synced via
8
+ the `agrippa` CLI. **You (the agent) never run `agrippa clone`, `agrippa pull`, or
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.
12
+
13
+ ---
14
+
15
+ ## CRM structure
16
+
17
+ ### MFAs and phases
18
+
19
+ The Odoo CRM uses a special module for exposing HTTP methods to the external world.
20
+ The module name is RIP, which lets developers define "Model Function Access" records.
21
+ Model Function Access (MFA) records contain Python code executed in `safe_eval`.
22
+
23
+ Phases are `symple.triplet.phase` records tied to `helpdesk.ticket` objects.
24
+ A helpdesk ticket has an associated `helpdesk.ticket.type`, which in turn has a
25
+ `symple.workflow`. A workflow has a sequence of `symple.triplet.phase` that the ticket
26
+ crosses in its lifecycle. There are many kinds of phases, but you only care about
27
+ automatic phases that have Python code inside. In the directory structure, unless the
28
+ current folder is named "mfa", each subdirectory is a workflow, and every Python file
29
+ inside corresponds to a Python phase with code.
30
+
31
+ For phases and MFAs you simply **edit the `.py` files** — there are no agrippa
32
+ commands for you to run; a human clones/pulls/pushes them.
33
+
34
+ ### Workflow structure (`workflow.yml`)
35
+
36
+ Each cloned workflow directory contains a `workflow.yml` next to its phase `.py`
37
+ files. It is **auto-generated, read-only context** describing how the workflow is
38
+ wired — the graph the editable phase files live inside. Read it to understand a
39
+ workflow before editing a phase; **never edit it** (it is regenerated on every
40
+ clone/pull and is not pushed anywhere). It does not contain phase Python code —
41
+ that lives in the `.py` files.
42
+
43
+ It holds:
44
+
45
+ - `workflow` — identity + flags (`process_type`, `is_tiqv`, `start_phase`,
46
+ `code_excluded_phases`, and the `triplets` / ticket-type details that enter it).
47
+ - `phases` — every phase as a graph node: `id`, `name`, `phase_code`,
48
+ `set_result_automatically`, `has_code_file` (true ⇒ an editable `.py` was cloned
49
+ for it), process/integration flags, `timeout`, `allowed_processes`,
50
+ `allowed_manual_phases`, and `results`.
51
+ - `results` (per phase) — the **outgoing edges**: each `result` has a `next_phase`
52
+ plus, for `from_code` phases, the `code_values` the phase's Python must assign to
53
+ `result` to take that edge (and `triplet_details` for `from_triplet` phases).
54
+
55
+ So to find "what value does this phase set to go to phase X", read `workflow.yml`:
56
+ match the result whose `next_phase` is X and use its `code_values`.
57
+
58
+ ### Code inside phases and MFAs
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 only — the 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`
89
+
90
+ ### Communication between phases
91
+
92
+ A key entity is `symple.pb.process.data`. This model stores arbitrary data (most
93
+ commonly JSON) in the `payload` field. It also has a `get_payload` method, which gives
94
+ you the parsed JSON it contains. Phases, MFAs and other components commonly communicate
95
+ via this model.
96
+
97
+ The key-value store (kv_store) is a special process-data record accessible via
98
+ `case_id.kv_store()`. It gives shorthand access to a process-data record unique to each
99
+ case. It exposes 4 methods:
100
+
101
+ - `get("key")`
102
+ - `set("key", "value")`
103
+ - `get_many("key", "key2")` → `a, b = case_id.sudo().kv_store().get_many("a, b")`
104
+ - `set_many({"a": 1, "b": 2})`
105
+
106
+ When possible, prefer the kv_store over manually creating and searching process-data
107
+ records.
108
+
109
+ ---
110
+
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.
118
+
119
+ > **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.
121
+
122
+ ### Decomposed project layout
123
+
124
+ ```
125
+ <wizard-dir>/
126
+ process.yaml identity + top-level flags (envelope)
127
+ structure.yaml THE graph: nodes hold their outgoing `edges` and nested
128
+ `nodes` (for subProcess/transaction); inline geometry
129
+ scripts/NNNN_*.js one scriptTask body each (byte-exact)
130
+ pages/<formKey>.yml one userTask page object each
131
+ .agrippa-pb.json manifest: scalars, namespaces, id↔file maps (do not hand-edit)
132
+ ```
133
+
134
+ `structure.yaml` is large and nested. **Do not read it to find ids** — use `pb ls`.
135
+ `agrippa-pb.json` is a massive json file. **Do not read it unless strictly required** — use `pb ls` and explore deconstructed files.
136
+
137
+ A node looks like:
138
+
139
+ ```yaml
140
+ - id: ScriptTask_0mmmti4
141
+ type: scriptTask
142
+ name: Init
143
+ script: scripts/0010_init.js # ref to the body file
144
+ layout: { x: -462, y: 78, width: 84, height: 84 }
145
+ edges: # OUTGOING flows belong to the source node
146
+ - id: SequenceFlow_1lso8x0
147
+ target: ExclusiveGateway_1lghfov
148
+ waypoints: [[-378, 120], [-345, 120]]
149
+ ```
150
+
151
+ Node types: `startEvent`, `endEvent`, `boundaryEvent`, `exclusiveGateway`,
152
+ `scriptTask`, `serviceTask`, `userTask`, `subProcess`, `transaction`.
153
+
154
+ ### Your loop
155
+
156
+ ```
157
+ agrippa pb ls # discover node/edge ids
158
+ agrippa pb add / rm / connect / disconnect / set-default # change the graph
159
+ # STOP. Do NOT format. Do NOT push. Report back to the human (see Formatting below).
160
+ ```
161
+
162
+ Every `pb` command targets **one** wizard. **Always pass `--pb <document_id>`** so you
163
+ never hit an interactive prompt.
164
+
165
+ ### Commands
166
+
167
+ #### `pb ls` — discover ids
168
+
169
+ ```bash
170
+ agrippa pb ls --pb <document_id>
171
+ ```
172
+
173
+ Flat list of every node with its id, type, name, parent (if nested), and outgoing
174
+ edges. **Start here** to find the ids you need. Example:
175
+
176
+ ```
177
+ ScriptTask_0mmmti4 (scriptTask) "Init"
178
+ → ExclusiveGateway_1lghfov (SequenceFlow_1lso8x0)
179
+ ExclusiveGateway_1lghfov (exclusiveGateway) "eg err"
180
+ → EndEvent_err (SequenceFlow_a) [default]
181
+ → ScriptTask_next (SequenceFlow_b) if ${isAlive}
182
+ ```
183
+
184
+ `[default]` marks a gateway's default flow; `if …` shows a flow condition.
185
+
186
+ #### `pb add` — add a node
187
+
188
+ ```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>
193
+ ```
194
+
195
+ Prints the new node id. Side effects, handled for you:
196
+
197
+ - `scriptTask` → creates an **empty** `scripts/NNNN_<slug>.js`; edit that file for the body.
198
+ - `userTask` → creates a **stub** `pages/<slug>.yml` + a manifest entry (no page content).
199
+ - `subProcess`/`transaction` → empty container; add children with `--parent <its-id>`.
200
+
201
+ The node is added **disconnected** with placeholder geometry. Connect it next.
202
+
203
+ **Insert mode** — `--from <id> --to <id>` splices the new node into an existing flow
204
+ instead of adding it disconnected:
205
+
206
+ ```bash
207
+ agrippa pb add --type scriptTask --name "New" --from A --to End --pb <document_id>
208
+ ```
209
+
210
+ Requires **exactly one** existing edge `A → End` already (errors if there's none, or
211
+ more than one — ambiguous). That edge is kept (same id, name, condition, gateway
212
+ `default` reference) and retargeted onto the new node; a second plain edge runs
213
+ new-node → `End`. `--parent` is implied by `A`/`End`'s container, so don't pass both.
214
+ `A`/`End` must be in the same container — it errors on boundary-crossing flows.
215
+
216
+ #### `pb rm` — remove a node
217
+
218
+ ```bash
219
+ agrippa pb rm --id ScriptTask_0mmmti4 --pb <document_id>
220
+ ```
221
+
222
+ Removes the node (and, for a container, its children), **every edge pointing at it**
223
+ (from anywhere in the graph), its own outgoing edges, and its script/page files +
224
+ manifest entries. No dangling references left behind.
225
+
226
+ #### `pb connect` — add a flow
227
+
228
+ ```bash
229
+ # plain sequence
230
+ agrippa pb connect --from ScriptTask_a --to ScriptTask_b --pb <document_id>
231
+
232
+ # gateway branch with a condition
233
+ agrippa pb connect --from ExclusiveGateway_g --to ScriptTask_b \
234
+ --condition '${isAlive}' --pb <document_id>
235
+
236
+ # the gateway's fallback branch
237
+ agrippa pb connect --from ExclusiveGateway_g --to EndEvent_err \
238
+ --default --pb <document_id>
239
+ ```
240
+
241
+ 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.
246
+
247
+ #### `pb disconnect` — remove a flow
248
+
249
+ ```bash
250
+ agrippa pb disconnect --id SequenceFlow_1lso8x0 --pb <document_id>
251
+ agrippa pb disconnect --from ScriptTask_a --to ScriptTask_b --pb <document_id>
252
+ ```
253
+
254
+ #### `pb set-default` — change a gateway's default flow
255
+
256
+ ```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>
259
+ ```
260
+
261
+ Flips an **already-existing** flow to be its source gateway's `default`, by edge id or
262
+ by `--from`/`--to` pair. Use this when a gateway already has a default and you want a
263
+ different outgoing flow to become it — no need to `disconnect` and re-`connect` just to
264
+ move the flag. The source must be an `exclusiveGateway`. Re-runs the gateway lint and
265
+ prints any `!` warnings (e.g. a now-default flow that still carries a condition, or a
266
+ non-default flow left without one).
267
+
268
+ #### `pb preview` — visual check
269
+
270
+ ```bash
271
+ agrippa pb preview --pb <document_id> --out /tmp/wizard.svg
272
+ ```
273
+
274
+ Renders the current geometry to an SVG so a human can eyeball the result. Safe to run.
275
+ Not byte-faithful to the real renderer — a sanity check only.
276
+
277
+ ### Formatting — a human decision, do NOT run it yourself
278
+
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
281
+ drastically different diagram. That may or may not be acceptable — **only the human
282
+ decides.**
283
+
284
+ So when you have added/connected blocks, the new nodes are left with placeholder
285
+ positions, and **you stop there**. Report to the human what you changed and that the
286
+ new blocks need positioning, then let them choose one of:
287
+
288
+ 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.
292
+
293
+ Never run `pb format` unless the human explicitly tells you to, with that trade-off
294
+ understood.
295
+
296
+ ### What to edit by hand (vs. commands)
297
+
298
+ Use the **commands** for graph structure (adding/removing nodes, wiring flows).
299
+ Edit the **files directly** for content within an existing node:
300
+
301
+ | Change | How |
302
+ | --- | --- |
303
+ | A scriptTask body | edit its `scripts/NNNN_*.js` file |
304
+ | A userTask page | edit its `pages/<formKey>.yml` file |
305
+ | A node's `name`, a flow's `condition`/`name`, serviceTask `fields`/`class` | edit `structure.yaml` for that node/edge |
306
+ | Identity/flags | edit `process.yaml` |
307
+
308
+ Never hand-edit `.agrippa-pb.json`, and never hand-assign `layout`/`waypoints`.
309
+
310
+ ### Recipes
311
+
312
+ **Insert a block into an existing path** (`A → B` becomes `A → New → B`, anywhere in
313
+ the graph, not just the end):
314
+
315
+ ```bash
316
+ agrippa pb add --type scriptTask --name "New" --from A --to B --pb W # → ScriptTask_new
317
+ # then edit scripts/NNNN_new.js with the body, and report back (see Formatting)
318
+ ```
319
+
320
+ Errors out instead of guessing if `A → B` doesn't have exactly one edge — fix the
321
+ graph (`pb ls`) first if so.
322
+
323
+ **Insert a decision branch** (gateway with two conditioned exits + a default):
324
+
325
+ ```bash
326
+ 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
328
+ agrippa pb connect --from ExclusiveGateway_g --to EndEvent_err --default --pb W
329
+ ```
330
+
331
+ **Add a subprocess with an inner step:**
332
+
333
+ ```bash
334
+ agrippa pb add --type subProcess --name "Retry" --pb W # → SubProcess_s
335
+ agrippa pb add --type scriptTask --name "Attempt" --parent SubProcess_s --pb W
336
+ ```
337
+
338
+ In every recipe: after the structural edits, **stop** and report — do not format, do
339
+ not push.
340
+
341
+ ### Safety
342
+
343
+ - `pb` commands are **local only** — nothing reaches upstream. Syncing
344
+ (`clone`/`pull`/`push`) and publishing are the human's job, never yours.
345
+ - After every command the project is re-checked for recomposability; a broken edit is
346
+ reported with a `WARNING`.
347
+ - When in doubt: `pb ls` to see the graph, make the structural change, `pb preview` to
348
+ show the human, then hand off.
349
+
350
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@waron97/prbot",
3
- "version": "3.1.3",
3
+ "version": "3.2.0",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -14,26 +14,30 @@
14
14
  "@inquirer/search": "^2.0.1",
15
15
  "commander": "^14.0.2",
16
16
  "dotenv": "^17.2.3",
17
+ "elkjs": "^0.11.1",
17
18
  "eslint-plugin-es5": "^1.5.0",
19
+ "fast-xml-parser": "^5.9.2",
18
20
  "inquirer": "^13.4.2",
19
21
  "inquirer-search-list": "^1.2.6",
20
22
  "node-fetch": "^3.3.2",
21
23
  "omelette": "^0.4.17",
22
- "prettier": "^3.5.3",
23
24
  "slugify": "^1.6.9",
24
25
  "yaml": "^2.9.0"
25
26
  },
26
27
  "devDependencies": {
27
28
  "@eslint/js": "^9.21.0",
29
+ "@ianvs/prettier-plugin-sort-imports": "^4.7.1",
28
30
  "@types/node": "^25.0.10",
29
31
  "@typescript-eslint/parser": "^8.26.0",
30
32
  "eslint": "^9.21.0",
33
+ "prettier": "^3.8.4",
31
34
  "typescript": "^5.8.2",
32
35
  "typescript-eslint": "^8.26.0"
33
36
  },
34
37
  "files": [
35
38
  "src/",
36
- "agrippa_typings/"
39
+ "agrippa_typings/",
40
+ "agrippa-pb.md"
37
41
  ],
38
42
  "keywords": [],
39
43
  "author": "",
@@ -1,21 +1,23 @@
1
- import inquirer from 'inquirer';
2
- import select from '@inquirer/select';
3
1
  import search from '@inquirer/search';
4
- import { readConfig, writeConfig, loadEffectiveEnv } from '../lib/config.js';
5
- import { listWorkflows, getPhasesByWorkflow, listMfas } from '../lib/api.js';
2
+ import select from '@inquirer/select';
3
+ import inquirer from 'inquirer';
6
4
  import { getToken } from '../../lib/auth.js';
7
- import { computeChecksum } from '../lib/checksum.js';
8
- import { toSlug, defaultMfaPath, writeCodeFile } from '../lib/workspace.js';
9
5
  import { fuzzyMatch } from '../../lib/fuzzy.js';
6
+ import { describeWorkflow, getPhasesByWorkflow, listMfas, listWorkflows } from '../lib/api.js';
7
+ import { computeChecksum } from '../lib/checksum.js';
8
+ import { loadEffectiveEnv, readConfig, writeConfig } from '../lib/config.js';
9
+ import { defaultMfaPath, toSlug, writeCodeFile, writeWorkflowDoc } from '../lib/workspace.js';
10
+ import { clonePb } from './clonePb.js';
10
11
 
11
12
  async function clone(opts) {
13
+ if (opts.pb) {
14
+ return clonePb(opts);
15
+ }
16
+
12
17
  const config = readConfig();
13
18
  loadEffectiveEnv(config);
14
19
 
15
- const ripUrl = process.env.RIP_URL;
16
- if (!ripUrl) throw new Error('RIP_URL is not configured. Run `prbot init` or set it in agrippa.yaml.');
17
-
18
- // Determine object type
20
+ // Determine object type (interactive prompt when no flag was passed)
19
21
  let objectType = opts.mfa ? 'mfa' : opts.phase ? 'phase' : null;
20
22
  if (!objectType) {
21
23
  objectType = await select({
@@ -23,9 +25,16 @@ async function clone(opts) {
23
25
  choices: [
24
26
  { name: 'MFA', value: 'mfa' },
25
27
  { name: 'Phase', value: 'phase' },
28
+ { name: 'Process Builder', value: 'pb' },
26
29
  ],
27
30
  });
28
31
  }
32
+ // Process-builder wizards have their own clone flow (PB_URL, recompose verify).
33
+ if (objectType === 'pb') return clonePb(opts);
34
+
35
+ const ripUrl = process.env.RIP_URL;
36
+ if (!ripUrl)
37
+ throw new Error('RIP_URL is not configured. Run `prbot init` or set it in agrippa.yaml.');
29
38
 
30
39
  console.log('Fetching records...');
31
40
  const token = await getToken();
@@ -52,9 +61,7 @@ async function clone(opts) {
52
61
  record = await search({
53
62
  message: `Select a ${objectType}:`,
54
63
  source: (input) => {
55
- const filtered = input
56
- ? records.filter((r) => fuzzyMatch(r.name, input))
57
- : records;
64
+ const filtered = input ? records.filter((r) => fuzzyMatch(r.name, input)) : records;
58
65
  return filtered.map((r) => ({
59
66
  name: objectType === 'mfa' ? `${r.model_name} / ${r.name}` : r.name,
60
67
  value: r,
@@ -102,6 +109,15 @@ async function clone(opts) {
102
109
  console.log(` wrote ${filePath}`);
103
110
  }
104
111
 
112
+ // Drop the workflow graph alongside the phase files as read-only context.
113
+ try {
114
+ const structure = await describeWorkflow(token, ripUrl, record.id);
115
+ const docPath = writeWorkflowDoc(basePath, structure);
116
+ console.log(` wrote ${docPath}`);
117
+ } catch (err) {
118
+ console.warn(` could not fetch workflow structure: ${err.message}`);
119
+ }
120
+
105
121
  console.log(`Cloned ${phases.length} phase(s) to ${basePath}/`);
106
122
  } else {
107
123
  const defaultPath = defaultMfaPath(record.model_name, record.name);