@pipefy/pipefy-process-coder 0.1.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 (92) hide show
  1. package/.agents/skills/ppc-pipefy-flow-authoring/SKILL.md +262 -0
  2. package/.agents/skills/ppc-pipefy-flow-authoring/examples/01-main-2-danfe-consulta.json +247 -0
  3. package/.agents/skills/ppc-pipefy-flow-authoring/examples/01-main-2-webhook-retorno-consulta.json +589 -0
  4. package/.agents/skills/ppc-pipefy-flow-authoring/examples/01-main-4-recebimento-barramento.json +1391 -0
  5. package/.agents/skills/ppc-pipefy-flow-authoring/examples/02-subflow-2-danfe-retorno.json +623 -0
  6. package/.agents/skills/ppc-pipefy-flow-authoring/examples/02-subflow-4-criacao-operacao.json +636 -0
  7. package/.agents/skills/ppc-pipefy-flow-authoring/examples/03-subflow-4-criacao-titulo.json +3642 -0
  8. package/.agents/skills/ppc-pipefy-flow-authoring/examples/04-subflow-4-criacao-cedente.json +863 -0
  9. package/.agents/skills/ppc-pipefy-flow-authoring/examples/04-subflow-4-criacao-sacado.json +799 -0
  10. package/.agents/skills/ppc-pipefy-flow-authoring/examples/README.md +42 -0
  11. package/.agents/skills/ppc-pipefy-flow-authoring/examples/hml-01-subflow-1-titulo-acompanhamento-cobranca.json +581 -0
  12. package/.agents/skills/ppc-pipefy-flow-authoring/examples/hml-01-subflow-1-titulo-nfe-monitoramento.json +503 -0
  13. package/.agents/skills/ppc-pipefy-flow-authoring/examples/hml-01-subflow-1-titulo-retorno-bancario.json +562 -0
  14. package/.agents/skills/ppc-pipefy-flow-authoring/examples/hml-02-subflow-2-retorno-consulta-cedente.json +557 -0
  15. package/.agents/skills/ppc-pipefy-flow-authoring/examples/hml-02-subflow-2-retorno-consulta-sacado.json +609 -0
  16. package/.agents/skills/ppc-pipefy-pipe-authoring/SKILL.md +146 -0
  17. package/.agents/skills/ppc-pipefy-process-design/SKILL.md +127 -0
  18. package/.agents/skills/ppc-pipefy-workspace/SKILL.md +91 -0
  19. package/AGENTS.md +456 -0
  20. package/README.md +808 -0
  21. package/bin/pipe.js +13 -0
  22. package/package.json +35 -0
  23. package/src/apply/adopt.ts +150 -0
  24. package/src/apply/agentops.ts +71 -0
  25. package/src/apply/compile.ts +875 -0
  26. package/src/apply/execute.ts +336 -0
  27. package/src/apply/flowops.ts +399 -0
  28. package/src/apply/idmap.ts +241 -0
  29. package/src/apply/mutations.ts +955 -0
  30. package/src/apply/registry.ts +430 -0
  31. package/src/apply/types.ts +134 -0
  32. package/src/cli/args.ts +88 -0
  33. package/src/cli.ts +211 -0
  34. package/src/codec/flow.ts +199 -0
  35. package/src/codec/pack.ts +103 -0
  36. package/src/codec/roundtrip.ts +94 -0
  37. package/src/codec/unpack.ts +198 -0
  38. package/src/commands/agents.ts +184 -0
  39. package/src/commands/apply.ts +1144 -0
  40. package/src/commands/context.ts +119 -0
  41. package/src/commands/create.ts +79 -0
  42. package/src/commands/diff.ts +314 -0
  43. package/src/commands/flows.ts +414 -0
  44. package/src/commands/misc.ts +644 -0
  45. package/src/commands/plan.ts +331 -0
  46. package/src/commands/pull.ts +567 -0
  47. package/src/commands/runs.ts +83 -0
  48. package/src/commands/skills.ts +137 -0
  49. package/src/commands/verify.ts +253 -0
  50. package/src/config.ts +168 -0
  51. package/src/diff/agents.ts +122 -0
  52. package/src/diff/diff.ts +1130 -0
  53. package/src/diff/flow.ts +318 -0
  54. package/src/diff/html.ts +322 -0
  55. package/src/diff/render.ts +101 -0
  56. package/src/model/payload.ts +154 -0
  57. package/src/model/tree.ts +99 -0
  58. package/src/model/volatile.ts +55 -0
  59. package/src/pipefy/agents.ts +165 -0
  60. package/src/pipefy/automations.ts +219 -0
  61. package/src/pipefy/capability.ts +119 -0
  62. package/src/pipefy/client.ts +267 -0
  63. package/src/pipefy/discovery.ts +209 -0
  64. package/src/pipefy/internal.ts +380 -0
  65. package/src/pipefy/ipaas.ts +365 -0
  66. package/src/pipefy/reconstruct.ts +775 -0
  67. package/src/pipefy/reference.ts +251 -0
  68. package/src/pipefy/snapshot.ts +245 -0
  69. package/src/pipefy/toolkit.ts +200 -0
  70. package/src/pipefy/toolkit_bearer.py +137 -0
  71. package/src/report/integrations.ts +231 -0
  72. package/src/report/run.ts +475 -0
  73. package/src/util/fsx.ts +45 -0
  74. package/src/util/git.ts +32 -0
  75. package/src/util/json.ts +55 -0
  76. package/src/util/log.ts +76 -0
  77. package/src/util/pool.ts +48 -0
  78. package/src/util/slug.ts +26 -0
  79. package/src/util/tui.ts +335 -0
  80. package/src/validate/index.ts +123 -0
  81. package/src/validate/integrity.ts +387 -0
  82. package/src/validate/reference.ts +136 -0
  83. package/src/validate/schema.ts +328 -0
  84. package/src/workspace/agents.ts +290 -0
  85. package/src/workspace/docs.ts +407 -0
  86. package/src/workspace/flows.ts +191 -0
  87. package/src/workspace/layout.ts +165 -0
  88. package/src/workspace/lock.ts +148 -0
  89. package/src/workspace/read.ts +165 -0
  90. package/src/workspace/reference.ts +24 -0
  91. package/src/workspace/stamp.ts +301 -0
  92. package/src/workspace/write.ts +225 -0
package/AGENTS.md ADDED
@@ -0,0 +1,456 @@
1
+ # AGENTS.md — working on pipefy-process-coder
2
+
3
+ Read this before changing anything. It is the accumulated cost of getting this
4
+ tool wrong in specific ways, written down so the next agent does not pay it again.
5
+
6
+ ---
7
+
8
+ ## 1. What this is, and what it is for
9
+
10
+ `pipe` is a CLI that pulls a Pipefy pipe — **and its iPaaS flows** — to disk as
11
+ readable JSON, lets you edit the files, diffs them, compiles the diff into API
12
+ calls, applies them, and verifies the result.
13
+
14
+ ```
15
+ pipe pull <pipeId> --out ./my-pipe # the pipe AND its iPaaS flows
16
+ # edit the JSON
17
+ pipe validate # shape + referential integrity, offline
18
+ pipe diff # what changed — pipe structure and flows
19
+ pipe plan --dry-run # is every change applicable? refuses if not
20
+ pipe apply # snapshot, apply step by step, verify, adopt
21
+ ```
22
+
23
+ **The app is the deliverable.** Whatever pipes appear in `workspaces/` are
24
+ *fixtures* — they exist to exercise the tool against a real endpoint, and they are
25
+ gitignored for a reason. Do not optimise for a good-looking pipe. A fixture change
26
+ that exercises an untested path is worth making; one that only makes the fixture
27
+ nicer is not.
28
+
29
+ The tool's job is to make a client's process **auditable and changeable as code**,
30
+ on a laptop, against an API that lies in specific documented ways. It is built for
31
+ an FDE working on someone else's Pipefy account, which is why every write is
32
+ planned, refusable and verified.
33
+
34
+ Specific ids appear in two places on purpose: `VALIDATION.md` and the run logs
35
+ under `workspaces/*/flows/BUILD-LOG.md` are **records of real runs** kept locally
36
+ and outside git, and an id
37
+ there is evidence. Everywhere else — source comments, help text, these docs, the
38
+ skills — ids are placeholders, because a general tool should not read as though it
39
+ were written for one customer.
40
+
41
+ ---
42
+
43
+ ## 2. The one rule that matters most
44
+
45
+ **Build through the app. Validate with the MCP afterwards.**
46
+
47
+ When `pipe` cannot do something you need, the answer is to *add it to `pipe`* —
48
+ not to reach for the Pipefy MCP, not to write a script in `research/` that does
49
+ the write for you. Routing around a gap hides exactly the gap that was worth
50
+ finding: the work looks finished and the tool is no better than before.
51
+
52
+ - A blocker met with a workaround teaches nothing.
53
+ - A blocker met by extending the app is the whole job.
54
+ - A limitation of the **platform** is worth reporting. A limitation of the **app**
55
+ is worth fixing.
56
+
57
+ The Pipefy MCP is for *independent verification after the build* — a different
58
+ transport and a different code path, which makes it a genuine second opinion. Use
59
+ `ap_validate_flow`, `ap_flow_structure`, `get_pipe`, `execute_graphql` to check
60
+ what the app did. Never to do it.
61
+
62
+ Anything that writes to a client's pipe belongs in `src/`, behind a command, with
63
+ a plan step and a verify. `research/` holds reads and probes.
64
+
65
+ Three probes there *do* write, because that is how the operation model and the
66
+ connection shapes were learned. Each refuses to run without `PIPE_PROBE_WRITES=1`,
67
+ and the reason is in §4. Do not add a fourth to get past a gap in the app.
68
+
69
+ ---
70
+
71
+ ## 3. The doctrines that make this trustworthy
72
+
73
+ These are not style. Each one exists because its absence caused a live incident.
74
+
75
+ ### Coverage: absence is not absence
76
+
77
+ Every entity group carries `complete | partial | unknown`. An `unknown` group
78
+ means the read path could not see it — **not** that the pipe has none. Such a
79
+ group is never diffed and never applied.
80
+
81
+ This is the difference between "this pipe has no webhooks" and "I cannot read
82
+ webhooks", and conflating them licenses deleting a client's live integrations.
83
+ Applies to iPaaS flows identically: a pipe whose iPaaS is off gets `unknown`, and
84
+ its flows are not compared at all.
85
+
86
+ ### Refuse rather than guess, per column
87
+
88
+ `src/apply/registry.ts` records, for every entity and operation, whether a write
89
+ path exists — and `settableColumns` records which *columns* it can actually
90
+ write. `pipe plan` refuses what has no path, naming the reason.
91
+
92
+ The failure this prevents: **a mutation that succeeds and changes nothing.**
93
+ Arguments are filtered against the live schema, so an argument the mutation has no
94
+ input for is silently dropped and the call returns success. `updatePipe` did this
95
+ with `description`, `anyone_can_create_card` and `create_card_label`. A workspace
96
+ that set one could never converge, and nothing said why.
97
+
98
+ ### Round trip or it does not ship
99
+
100
+ `pack(unpack(P)) == P` for the payload codec, `link(unlink(F)) == F` for the flow
101
+ codec. The payload fold is *movement only*, which makes it true by construction;
102
+ the flow fold is a real transformation, so `test/flow-codec.test.ts` earns it
103
+ across a linear chain, a router with an empty branch, a loop with a body, a router
104
+ nested in a loop, and the live flows.
105
+
106
+ If you add a fold, prove it the same way or keep the raw JSON.
107
+
108
+ ### Verify against intent, not identity
109
+
110
+ After an apply, the tool reads the pipe back and diffs `live → expected` with
111
+ `intentOnly`. An authored entity is a *partial specification*: the author writes
112
+ what they care about and the server fills the rest. Asking "are these rows
113
+ identical" fails a perfectly correct apply.
114
+
115
+ Never assert a column the server mints and no create path accepts — `uuid`,
116
+ `slug`, `color_uuid`, `suid`. That mistake reported 60 successful creates as
117
+ failures.
118
+
119
+ ### Nothing leaves carrying a placeholder
120
+
121
+ A new entity has no id, so a reference to it is `%{_new:<uuid>}`, substituted at
122
+ send time from `IdMap`. `ID_BEARING_KEYS` governs which keys get a bare-value
123
+ substitution, and the executor **refuses to send any request still containing
124
+ `_new:`**, naming the key path.
125
+
126
+ That list has been the cause of three separate bugs — `fieldConditionIds`, then
127
+ `phase[jump_target_ids][]`. When you add a key that holds an id, add it there.
128
+
129
+ ### The create response already has the id — a read-back is not the only way to learn it
130
+
131
+ `IdMap` is built live during `execute.ts`, straight from each create mutation's
132
+ own response, to resolve `%{_new:<uuid>}` for later steps in the same run. That
133
+ map is also enough to adopt a created entity's id into the workspace file it
134
+ came from (`apply/adopt.ts`) — no live read-back needed, and by construction
135
+ never wrong, because it is the id the mutation that created the entity actually
136
+ returned.
137
+
138
+ This is why `pipe apply` no longer snapshots a second time on its own
139
+ initiative: that second, live-read snapshot (`SnapshotClient.pull()` always
140
+ creates one — snapshot.ts's own comment says so) was the thing making the
141
+ pre-apply rollback snapshot stop being restorable the moment the apply that
142
+ took it finished. Id adoption never needed the read; only a *fuller*
143
+ comparison (server-minted `slug`, `color_uuid`, and the like) does, and that
144
+ stays opt-in (`--post-snapshot`, or the prompt).
145
+
146
+ Every `produces` entry on a create step now matters for this reason too, not
147
+ only for cross-step reference resolution — a create with no `produces` (labels,
148
+ webhooks and pipe_relations lacked one until this was noticed live: `pipe
149
+ apply` reported the id adopted, and the file still read `"id": null, "_new":
150
+ true"`) silently cannot be adopted this way.
151
+
152
+ ### One apply, one revert question — not three mechanisms wearing one word
153
+
154
+ The first version of this had three unrelated things all called some kind of
155
+ "rollback": a pipe-structure offer wired to `pipe apply`'s pipe-side failure,
156
+ a flow-only offer wired to the flow half, and nothing at all for agents. That
157
+ is not how a user experiences an apply — it is one action, and if any part of
158
+ it fails partway, the honest question is "revert everything this apply
159
+ changed", asked once, with the details of what that actually means for each
160
+ part that touched anything. `offerUnifiedRevert` (apply.ts) is that one
161
+ question; `finishApply` is what makes sure every exit from `applyOneRepo`
162
+ that could still touch flows or agents runs through it.
163
+
164
+ It is also asked when the failure is *not* in the half that already
165
+ succeeded: a pipe change that lands cleanly and is then followed by a flow or
166
+ agent failure still gets offered as part of the same revert, because from the
167
+ user's side "this apply" covers all of it, not just whichever half happened
168
+ to error.
169
+
170
+ Each part is reverted the way its own platform actually allows, because
171
+ there is no single API that reverses all three:
172
+
173
+ - **Pipe structure** — `restoreRepoToSnapshot`, Pipefy's own mechanism,
174
+ restoring the pre-apply snapshot. Only offered while that snapshot is still
175
+ the latest (`caps.restoreLatestOnly`) — nothing before this point takes a
176
+ second snapshot on its own initiative, so it normally still is.
177
+ - **iPaaS flows** and **AI agents** have no snapshot at all — a rollback for
178
+ either is filling a real gap this tool owns, not routing around a platform
179
+ one. Each is reverted by diffing the *current* live state (re-read fresh,
180
+ since some steps may have landed) against the read taken right before this
181
+ apply touched it, direction flipped: current is baseline, the old
182
+ pre-apply state is the target. `buildFlowRevertPlan` / `planFlowRevert`
183
+ (commands/flows.ts) and `buildAgentRevertChangeSet` / `planAgentRevert`
184
+ (commands/agents.ts) — the agent diff is otherwise offline against the
185
+ local baseline (module doc comment there), so its revert point is a live
186
+ read this apply now takes specifically to have one, the same gap flows
187
+ closed first.
188
+
189
+ Rendered in full for every part that has anything to revert, before the one
190
+ question is asked, and executed — on confirmation — in the reverse of the
191
+ order they were applied: agents, then flows, then pipe structure.
192
+
193
+ None of this reaches further than the platform allows, and says so rather
194
+ than implying otherwise: no revert, Pipefy's or this tool's, un-sends an
195
+ email an automation already dispatched or un-fires a webhook that already
196
+ notified a third party. That is the platform's own irreversibility, not
197
+ something a bigger rollback would fix.
198
+
199
+ Verified live, for flows: a real ADD_ACTION rejection, the correct reverse
200
+ plan (delete what landed, restore the old link), and the flow back to its
201
+ exact original chain once applied — twice, once through the standalone
202
+ mechanism and once through the unified one that replaced it.
203
+
204
+ ### Read the bytes back
205
+
206
+ The check nothing else can make. After a flow write, every stored datapill is
207
+ compared against what was sent. A migrating write path rewrites
208
+ `{{step_1['output'].x}}` into `{{step_1['output']['output'].x}}` — structurally
209
+ valid, accepted by Pipefy's own validator, and resolving to nothing at runtime.
210
+
211
+ Generalise the habit: when a write path might transform what you send, compare the
212
+ stored value, not the status code.
213
+
214
+ ---
215
+
216
+ ## 4. How this endpoint lies
217
+
218
+ `VALIDATION.md` is the full catalogue — 30 corrected API facts, 34 bugs found by
219
+ live running. Do not re-derive them. The shapes that bite most often:
220
+
221
+ | | |
222
+ | --- | --- |
223
+ | A mutation can **succeed and change nothing** | arguments with no input are dropped silently |
224
+ | `updatePipe.title_field_id` | wants the field's **uuid** or slug, never the numeric id the payload holds |
225
+ | `archiveField` | `{ uuid }`, not the id |
226
+ | `restoreRepoSnapshot` | **does not exist.** The endpoint has **`restoreRepoToSnapshot`**, which this build never looked for — so every apply printed "no one-command rollback" beside a snapshot that was restorable |
227
+ | `restoreRepoToSnapshot.versionId` | the **latest** snapshot only. An older one is refused with `Version_id must reference the latest uploaded snapshot of the Pipe`, so a pre-apply snapshot stops being restorable the moment the verify read-back snapshots the repo. Recovery is forward-first |
228
+ | a restore | **asynchronous**, reported through `operations` as `RESTORE`, not by the mutation returning |
229
+ | `pipe(id:)` on a database | **answers**, and returns its record statuses as phases. `Table.url` matching `/apollo_databases/` is the only discriminator |
230
+ | a database's snapshot payload | **is a pipe's** — the same twenty relation arrays, columns under `fields` with a real start-form `phase_id`. `createRepoSnapshot` accepts a table id |
231
+ | `updateTableField` / `deleteTableField` / `setTableFieldOrder` | the field's **slug**, with `table_id` alongside. The internal id and uuid are both refused — the inverse of `updatePhaseField` |
232
+ | `updateTable.summary_attributes[]` | the field's **internal id**, refusing the slug every neighbouring argument wants |
233
+ | `CreateTableFieldInput.type` | its description lists 23 values and **under-reports**: `dynamic_content` is accepted. `formula` is readable and refused. `FieldTypeId` is the authorable set |
234
+ | `createLabel` for a database | **`table_id`, not `pipe_id`** — the latter answers `Pipe not found with id: <the database id>`, which reads like a missing capability. Full CRUD once addressed right |
235
+ | `createWebhook` for a database | **accepted, and it attaches** — but the product offers no webhooks on a database and the actions are card-shaped, so one sits there inert. Refused deliberately, not by omission |
236
+ | a table field label | **unique per table** — a duplicate is refused with `Field label has already been taken`, so `label` is a sound natural key |
237
+ | a phase field label | `createPhaseField` **does not** enforce this — two fields both labelled "Classification" were created live, one in the same phase as the first, and the slug was disambiguated instead. `updatePhaseField.label` is non-null, though, so it resends the current label on *every* call regardless of what changed, and that is where a pre-existing duplicate surfaces as the same `Field label has already been taken` — caught offline now in `validate/schema.ts` |
238
+ | `deletePhaseField` | the field's **slug**, not the numeric id. A field has three identifiers and three mutations disagree: `updatePhaseField.id` takes the internal id, `archiveField` the uuid, this one the slug. Sent the id it says `Field not found with id: <id>` about a field that is plainly there |
239
+ | `phaseSettings` | reports **failure for changes it has already made** — three runs, three times. Its errors are inconclusive; re-read. |
240
+ | `AutomationActionParamsInput` | mixes snake and camel: `card_id`, `field_map`, but `taskParams`, `httpMethod` |
241
+ | pipe relation flags | `canCreateNewItems` in the mutation, `can_create_connected_cards` in the payload |
242
+ | `hidden_top_buttons` | a closed set of six, documented only in its own rejection |
243
+ | automation event/action params | a wrong one is rejected as **`is invalid`** and nothing else, on both endpoints. The catalogue (`automationEvents`, `automationActions`) is public — read it and refuse by name |
244
+ | an automation `condition` | **silently dropped** unless every expression carries `structure_id` — `expressions_structure` is a list of structure ids, not indices. Mutation succeeds, `expressions: []` stored |
245
+ | `createFieldCondition` without `condition` | **`"Something went wrong"`**. An unconditional hide is real and useful, but the key must be present as `{expressions: []}`. `condition` is not NON_NULL on the input type |
246
+ | `setFieldConditionOrder` | answers a **no-op with `"Something went wrong"`**. A new condition is appended by the server, so appending one in the workspace needs no reorder — compare against baseline + this phase's creates, not the baseline alone |
247
+ | `createFieldCondition.phaseId` | **authorized and then ignored.** A bogus id is refused with "Permission denied", a real one is accepted, and the condition lands on the start form regardless — even when both fields it names live on the phase asked for. The UI can target any phase; this mutation cannot |
248
+ | an expression with no `structure_id` | `Invalid input: Structure can't be blank`. `expressions_structure` names structure ids; derive one from position when the author gives none |
249
+ | a nested collection in verification | server-minted ids, an added `phase_id`, and `0` vs `"0"` in `expressions_structure` are **not drift**. Reduce the live side to what the intent states, at every depth, or a created condition can never verify |
250
+ | a field condition testing the field it hides | stored happily, and the **phase form stops opening**. The API validates none of this — not the self-reference, not the operation, not even `not_a_real_operation` |
251
+ | `automationVariables.active` | omitted for the life of the tool, so disabling an automation was a no-op that reported success |
252
+ | `updateAutomation` | works, across every field map dimension. The recipes' delete-and-recreate was inherited, not measured |
253
+ | iPaaS `IMPORT_FLOW` | migrates the tree it is given; never use it |
254
+ | iPaaS connections | the host marks any credential `ACTIVE` **without validating it** |
255
+ | `POST /app-connections/{id}` | accepts `scope` and `projectIds` and returns 200 ignoring both |
256
+ | iPaaS `ADD_ACTION`, parent is a `LOOP_ON_ITEMS` | `stepLocationRelativeToParent` is **required**, not optional, the moment the parent is a loop. Omitted (the shape every non-loop add uses, and the only shape ever probed before this) it answers `Loop step parent undefined not found` on the add that exits the loop — entering it (`INSIDE_LOOP`) already carried the field and already worked. `'AFTER'` sent explicitly fixes it |
257
+ | a flow datapill reading a `connector` field's value | **the connected card(s)' id(s) only** — never their own field values, at any depth you index into the payload. Reading a connected card's fields needs an explicit `getCardById` (or similar) step per id; the parent's connector-field datapill can never supply them |
258
+
259
+ ### Closed sets: the list is published and enforced by nothing
260
+
261
+ The schema publishes every fixed list an authored value must come from —
262
+ `AutomationsEvents` (10), `AutomationsActions` (15), `FieldTypeId` (24), `Colors`
263
+ (12) — and then types the input fields that consume them as `ID` and `String`:
264
+
265
+ ```
266
+ CreateAutomationInput.event_id ID
267
+ CreateAutomationInput.action_id ID
268
+ CreatePhaseFieldInput.type_id String
269
+ ```
270
+
271
+ So GraphQL validates none of them. A wrong value passes query validation, reaches
272
+ a runtime check, and returns one of `is invalid` or `All fields must be filled
273
+ properly.` — naming no parameter, no field, and no accepted value, partway
274
+ through an apply.
275
+
276
+ The traps are near-misses, which is why reading the list beats knowing the domain.
277
+ All three of these were live:
278
+
279
+ | written | why it failed |
280
+ | --- | --- |
281
+ | `card_moved` + `event_params.in_phase_id` | real parameter, wrong event — it belongs to `card_inbox_received_email`. `card_moved` takes only `to_phase_id`. |
282
+ | `move_single_card` + `action_params.card_id` | real parameter, wrong action — accepted on `update_card_field`, refused here. Verified by probe. |
283
+ | a `time_range` field | in this tool's own hardcoded list; **not** a type the API offers |
284
+
285
+ **Never hardcode one of these lists.** That was the previous approach and it
286
+ rotted silently: `FIELD_TYPES` in `validate/schema.ts` carried `time_range`, which
287
+ does not exist, and omitted `dynamic_content`, which does — warning about a valid
288
+ type and passing an invalid one, with nothing to reveal either. It survives only
289
+ as a fallback for workspaces with no reference, and only as a warning.
290
+
291
+ Instead: `pipefy/reference.ts` reads the sets on every pull into
292
+ `.ppc/reference.json`, `validate/reference.ts` refuses offline and names the
293
+ value, and `REFERENCE.md` puts the same lists in the workspace so whoever edits
294
+ the JSON reads what is accepted instead of guessing. A new closed set is three
295
+ lines — add the enum to `TRACKED_ENUMS`, add a site to `ENUM_SITES`. **Absent
296
+ reference means the check is skipped, never that a value is valid.**
297
+
298
+ Pipefy's own docs for the automation mutations are worth reading once
299
+ ([create](https://developers.pipefy.com/reference/automation-creation),
300
+ [update](https://developers.pipefy.com/reference/automation-update),
301
+ [delete](https://developers.pipefy.com/reference/automation-deletion),
302
+ [retrieve](https://developers.pipefy.com/reference/automation-retrieve)), but they
303
+ are secondary to the generated file: they describe the mutations, not which
304
+ parameters this pipe's events and actions accept, and they cannot go stale in a
305
+ way `pipe pull` would notice.
306
+
307
+ ### Check the endpoint before assuming the internal API
308
+
309
+ All three automation mutations are **public** and were being sent to the internal
310
+ endpoint — inherited from the Change Migrator recipes and never re-checked, which
311
+ made automations the one entity written through an unversioned endpoint with no
312
+ introspection, for no reason. Before adding anything to `registry.ts` with
313
+ `transport: 'internal'`, introspect the public schema for it. If it genuinely is
314
+ not there, say which public mutation is missing and how that was verified.
315
+
316
+ ### Probing is not free on this platform
317
+
318
+ Two separate incidents from the same wrong assumption — that schema validation
319
+ rejects a malformed request before anything acts:
320
+
321
+ - Enumerating the flow-operation enum with `request: {}` **published and enabled a
322
+ draft flow**. Five operations act on an empty request: `CHANGE_STATUS`,
323
+ `LOCK_AND_PUBLISH`, `LOCK_FLOW`, `CHANGE_FOLDER`, `UPDATE_METADATA`.
324
+ - Enumerating connection types with `"probe"` as the credential returned
325
+ **201 ACTIVE** every time, leaving a connection that would fail at runtime.
326
+
327
+ **Probe reads freely. Assume every write-shaped probe is real**, do it on a
328
+ throwaway object, and check the state afterwards. Record what you learn in
329
+ `VALIDATION.md`.
330
+
331
+ ---
332
+
333
+ ## 5. Where things live
334
+
335
+ ```
336
+ src/
337
+ cli.ts command dispatch
338
+ config.ts endpoints, credential resolution, limits
339
+ pipefy/ graphql client · internal API · snapshots · reconstruction ·
340
+ discovery · capability detection · ipaas (Advanced Automations) ·
341
+ reference (the closed sets, read per pull)
342
+ codec/ unpack · pack · round-trip · flow (linked list <-> nodes)
343
+ workspace/ layout · read · write · lock · flows · stamp · generated docs
344
+ validate/ shape · referential integrity · closed sets, offline
345
+ diff/ pipe change set · flow change set · text render · HTML view
346
+ apply/ registry · mutation builders · compiler · id map · executor ·
347
+ flowops (iPaaS operations)
348
+ report/ run records · a pipe's integration surface
349
+ commands/ one file per command
350
+ test/ 116 tests, node:test, no framework
351
+ research/ probes and reads. The three that write are gated behind
352
+ PIPE_PROBE_WRITES=1 and say why.
353
+ .agents/skills/ four skills for agents editing workspaces
354
+ ```
355
+
356
+ **Docs, in the order they answer questions:**
357
+
358
+ | | |
359
+ | --- | --- |
360
+ | `README.md` | what the tool does, install, the loop, token costs, integrations |
361
+ | `VALIDATION.md` | every API fact and every bug found by running it live |
362
+ | `PLAN-IPAAS.md` | the iPaaS plan, its evidence, and what is still unbuilt |
363
+ | `PLAN-BEHAVIOUR.md` | **plan, unimplemented.** Why a valid, stored automation can be inert, and the layers that would catch it |
364
+ | `PLAN.md`, `PLAN-ROUTE-B.md`, `ROUTE-B-V2.md` | the original design and its revisions |
365
+ | `.agents/skills/*/SKILL.md` | how to *use* the tool on a workspace |
366
+ | a workspace's `REFERENCE.md` | generated, not written: what the API accepts in every closed-set position |
367
+
368
+ ---
369
+
370
+ ## 6. Conventions
371
+
372
+ **Zero runtime dependencies, no build step.** Every import in `src/` is a `node:`
373
+ builtin. Node ≥ 22.18 strips the types itself. Verified by deleting
374
+ `node_modules` and running the CLI. Do not add a dependency; an FDE behind a proxy
375
+ that blocks the npm registry has to be able to clone and run.
376
+
377
+ `erasableSyntaxOnly` is on: **no constructor parameter properties, no enums, no
378
+ namespaces.** Declare fields explicitly.
379
+
380
+ **Comments explain why, not what.** The codebase is dense with them and they are
381
+ load-bearing: most record an API fact or a failure that motivated the code. When
382
+ you fix something subtle, leave the reason. When you read a comment that says an
383
+ argument is dropped or an error is inconclusive, believe it — it was measured.
384
+
385
+ **Tests are `node:test`, no framework.** `node --test test/*.test.ts`. Add a case
386
+ for the shape of a bug, not just the fix. `test/flow-codec.test.ts` and
387
+ `report.test.ts` are the models: they break things deliberately and assert the
388
+ finding.
389
+
390
+ **Commit messages describe the change and why, in prose.** They are long here on
391
+ purpose — a commit is where the reason for a subtle fix survives. Describe the
392
+ change, never the tooling that produced it.
393
+
394
+ **Line endings are mixed** (CRLF and LF). Multi-line string matching against
395
+ source files fails on the wrong one; match single lines or normalise first.
396
+
397
+ **`workspaces/` is gitignored** because a pulled pipe carries webhook URLs, which
398
+ are bearer-equivalent. Force-add only files you have checked, and say why.
399
+
400
+ ---
401
+
402
+ ## 7. Things not to do
403
+
404
+ - **Do not delete-and-recreate to work around a missing write path.** It destroys
405
+ every existing value. `pipe plan` refuses these on purpose; say so and stop.
406
+ - **Do not enable an iPaaS flow as a side effect.** It arms the flow against live
407
+ traffic. `--allow-flow-enable` exists so it is always deliberate, and it is
408
+ additionally refused while the project holds a placeholder connection.
409
+ - **Do not write an iPaaS connection's credential.** Connections hold secrets:
410
+ read them, reference them, refuse to write them. A *placeholder* connection is
411
+ the exception and is tagged as such.
412
+ - **Do not send `IMPORT_FLOW`.** `IpaasClient.operate` refuses it. See §3.
413
+ - **Do not trust `ACTIVE`, a 200, or a validator.** All three have signed off on
414
+ broken states here.
415
+ - **Do not treat a missing prerequisite as a reason not to build.** A pipe with no
416
+ iPaaS connections gets a placeholder and the flow gets built; the structure is
417
+ what is being authored and it does not depend on the credential working.
418
+
419
+ ---
420
+
421
+ ## 8. What is still unbuilt
422
+
423
+ Honest gaps, so you do not promise them:
424
+
425
+ - **Check the endpoint before assuming the internal API.** Automations were sent
426
+ to the internal endpoint for the whole life of this project because the
427
+ Change Migrator recipes had to, and nobody re-checked. All three mutations are
428
+ public and documented. If something is on the internal API, there should be a
429
+ comment saying which public mutation is missing and how that was verified.
430
+ - ~~**Databases are discovered but not pulled.**~~ Done. A database is a repo:
431
+ its columns are fields on a start form and the phases after it are record
432
+ statuses, so the existing codec round-trips one with nothing added. What is
433
+ *not* done is records, excluded by decision rather than omission. Labels are
434
+ supported; webhooks are refused on purpose, with the measurement in the reason.
435
+ - **`setFieldConditionOrder`** works but is the least exercised write path.
436
+ - ~~**Flow node *creation* inside an existing flow** is refused by the compiler.~~
437
+ Done. `diff/flow.ts` attaches the authored tree to a `node.create` change so
438
+ `apply/flowops.ts` can find the new node's parent the same way
439
+ `buildFlowOperations` already does for a brand-new flow — whoever's
440
+ `_nextAction` / `_firstLoopAction` / a branch's `_children` names it — and
441
+ sends `ADD_ACTION`. A node nothing in the chain points to is still refused,
442
+ by name, rather than guessed at. Multiple new nodes in one apply are fine as
443
+ long as each is reachable once its own parent (new or old) exists, which the
444
+ file's execution-order-as-topological-order guarantee already provides.
445
+ - **`pipe validate` does not yet check flows.** The rules are specified in
446
+ `PLAN-IPAAS.md` §6 Phase 2 and prototyped in `research/ipaas/validate-flow.ts`
447
+ with 17 negative tests. Moving them into `src/validate/` is the next obvious
448
+ step, and it is where the cross-surface checks belong — a datapill naming a
449
+ field by slug that the pipe does not have, a write whose value cannot land in
450
+ that `type_id`, deleting a field a live flow depends on.
451
+ - **A desktop shell** is not coming. `PLAN.md` phase 3 described an Electron app;
452
+ it was removed rather than deferred, because `pipe diff --open` renders the
453
+ same HTML in the browser and a GUI is a second product to keep working. If a
454
+ UI is ever wanted, it belongs in its own repo consuming this one.
455
+
456
+ When you close one of these, update this section and `PLAN-IPAAS.md` together.