n8n-decanter 0.7.0 → 0.10.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/CHANGELOG.md +584 -0
- package/README.md +46 -74
- package/dist/lib/api.mjs +35 -0
- package/dist/lib/compile.mjs +11 -2
- package/dist/lib/config.mjs +12 -1
- package/dist/lib/diff.mjs +75 -3
- package/dist/lib/engine.mjs +3 -2
- package/dist/lib/errors.mjs +20 -0
- package/dist/lib/executions.mjs +5 -5
- package/dist/lib/init.mjs +9 -4
- package/dist/lib/lifecycle.mjs +16 -0
- package/dist/lib/mcp.mjs +23 -3
- package/dist/lib/mcpconnect.mjs +32 -4
- package/dist/lib/mcpserve.mjs +90 -2
- package/dist/lib/mirror.mjs +13 -2
- package/dist/lib/picker.mjs +95 -2
- package/dist/lib/preflight.mjs +162 -73
- package/dist/lib/prompt.mjs +3 -3
- package/dist/lib/pull.mjs +29 -6
- package/dist/lib/push.mjs +8 -4
- package/dist/lib/run.mjs +36 -5
- package/dist/lib/simulate.mjs +210 -29
- package/dist/lib/state.mjs +11 -2
- package/dist/lib/status.mjs +17 -87
- package/dist/lib/style.mjs +44 -1
- package/dist/lib/testrun.mjs +54 -20
- package/dist/lib/util.mjs +48 -23
- package/dist/lib/validate.mjs +174 -26
- package/dist/n8n-decanter.mjs +209 -214
- package/dist/scripts/typecheck.mjs +15 -1
- package/package.json +4 -4
- package/template/.claude/hooks/mcp-route-check.mjs.example +1 -1
- package/template/.claude/hooks/rename-refs.mjs.example +126 -0
- package/template/.claude/hooks/verify.mjs.example +26 -8
- package/template/.claude/settings.json.example +30 -7
- package/template/.mcp.json.example +2 -2
- package/template/AGENTS.md.example +193 -91
- package/template/CLAUDE.md.example +11 -7
- package/template/decanter-ts-plugin/index.js.example +1 -1
- package/template/opencode.json.example +5 -2
- package/template/package.json.example +2 -2
- package/template/tsconfig.json.example +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,590 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.10.0] - 2026-08-07
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`scenario create <workflow> "<slug>" --extend`** — top an **existing**
|
|
15
|
+
scenario up with the pinnable nodes it is missing, keeping every value already
|
|
16
|
+
authored. Previously `scenario create` refused an existing file outright, so a
|
|
17
|
+
scenario `test` rejected could only be fixed by hand-editing raw JSON for nodes
|
|
18
|
+
the tool had never named. Also covers the ordinary case of a workflow that
|
|
19
|
+
gained a node after its scenario was written.
|
|
20
|
+
|
|
21
|
+
- **`scenario create --scaffold` now works with no instance.** The fill entries
|
|
22
|
+
were always built from your local `workflow.json`; the instance only supplied
|
|
23
|
+
the per-node output **JSON Schemas**, which annotate the fill rather than
|
|
24
|
+
enable it. With no `N8N_HOST` configured it now says the annotations are
|
|
25
|
+
missing and scaffolds anyway — each node lands as provenance `authored`
|
|
26
|
+
instead of `scaffolded`, so the difference stays visible in the file. This is
|
|
27
|
+
what makes `preflight --offline --simulate` reachable on a plane: of the four
|
|
28
|
+
pin sources, only *fetching a fresh capture* actually needs n8n. The messages
|
|
29
|
+
that route you to a pin source now lead with the offline-viable ones, and
|
|
30
|
+
`docs/cli/preflight.md` states which of the four need the instance.
|
|
31
|
+
|
|
32
|
+
### Changed
|
|
33
|
+
|
|
34
|
+
- **Breaking:** **a slug-less `scenario create --scaffold` now writes
|
|
35
|
+
`scenarios/scaffold.json`, not `scenarios/scenario.json`.** The old default collided with the `scenario`
|
|
36
|
+
verb, and the flag parser refuses to read a verb name as a flag value — so
|
|
37
|
+
`preflight --simulate --scenario scenario` failed with `--scenario needs a
|
|
38
|
+
value`, leaving the default file referenceable only as `--scenario=scenario`.
|
|
39
|
+
If you have a script that names the old default file, point it at the new one
|
|
40
|
+
(an explicit `<slug>` argument was, and stays, unaffected).
|
|
41
|
+
|
|
42
|
+
- **`scenario check` now reports the `test` gate too, not just the
|
|
43
|
+
`preflight --simulate` one.** The two demand different node sets on purpose —
|
|
44
|
+
`--simulate` asks only for nodes the capture **reached**, `test` asks for
|
|
45
|
+
**every** enabled non-pure node because it runs on the live instance with real
|
|
46
|
+
credentials. Reporting only the looser one meant a scenario could be green and
|
|
47
|
+
still be refused by `test`. `check` now says which gate you have passed, and
|
|
48
|
+
names `--extend` as the way to close the difference. The docs that asserted the
|
|
49
|
+
two rules were the same are corrected.
|
|
50
|
+
|
|
51
|
+
- **`scenario create --execution` pins unreached nodes to an empty run instead of
|
|
52
|
+
asking you to invent output for them.** A pinnable node the capture never
|
|
53
|
+
reached is written as `[{"data":{"main":[[]]}}]` — "this branch isn't
|
|
54
|
+
exercised" — and listed under `_decanterScenario.notExercised`. That makes a
|
|
55
|
+
capture-seeded scenario usable with `test` straight away, keeps the node pinned
|
|
56
|
+
to zero items (so it can never touch the real world), and leaves the claim
|
|
57
|
+
visible for review: if a branch *should* have run, give it real data.
|
|
58
|
+
|
|
59
|
+
- **The cold-start errors now name the non-interactive `init`, and `--mcp-token`
|
|
60
|
+
is an accepted alias for `--token`.** A fresh clone has no `.env` (it is
|
|
61
|
+
gitignored), so `N8N_HOST must be set …` is the first thing you — or a coding
|
|
62
|
+
agent — read. It said what was wrong and pointed only at a path that needs
|
|
63
|
+
someone at a prompt; a blind session diagnosed the problem in one command and
|
|
64
|
+
then had to hand the job back to a human. Both messages now spell out
|
|
65
|
+
`n8n-decanter init . --host <host-url> --token <mcp-token>`, `init --host` on
|
|
66
|
+
its own names `--token` when it warns about missing credentials, and the token
|
|
67
|
+
flag accepts either spelling.
|
|
68
|
+
|
|
69
|
+
### Fixed
|
|
70
|
+
|
|
71
|
+
- **The "install typescript" advice now pins `@^5`.** A bare
|
|
72
|
+
`npm i -D typescript` installs **7.x**, whose compiler is the native rewrite
|
|
73
|
+
and no longer exposes the programmatic API decanter's node-file typecheck
|
|
74
|
+
drives — so following the old advice replaced a *skipped* check with a broken
|
|
75
|
+
one. The skip message and `preflight`'s unlock now both say
|
|
76
|
+
`npm i -D typescript@^5` and name the reason. `init`'s scaffold already pinned
|
|
77
|
+
`^5`; this only ever bit projects `init` deliberately left alone.
|
|
78
|
+
|
|
79
|
+
- **A 403 from the public API now names the scope you are missing.** n8n answers
|
|
80
|
+
a valid-but-under-scoped `N8N_API_KEY` with a bare 403 and says nothing about
|
|
81
|
+
which of eight scopes is absent. Every REST surface — executions, data tables,
|
|
82
|
+
backup — now gets a per-endpoint hint, including the trap that catches people
|
|
83
|
+
out: `dataTable:read` does **not** cover `/columns` or `/rows`, which need
|
|
84
|
+
`dataTableColumn:read` and `dataTableRow:read`. The data-table hints also say
|
|
85
|
+
outright that decanter only ever **reads** them, so no write scope is needed.
|
|
86
|
+
|
|
87
|
+
- **`scenario create` prints the file's size, and warns before it lands in git.**
|
|
88
|
+
A capture-seeded scenario is a verbatim copy of every item of every node; one
|
|
89
|
+
from a busy production run can be tens of megabytes. Nothing measured it, and
|
|
90
|
+
`scenarios/` is tracked — so the folder-wide auto-commit on the next `pull` or
|
|
91
|
+
`push` swept it into history unasked. The success line now carries the size,
|
|
92
|
+
and anything above 1 MB warns explicitly that it is about to be committed.
|
|
93
|
+
|
|
94
|
+
- **Three scenario messages pointed somewhere the thing you needed was not.**
|
|
95
|
+
A pre-rename scenario (`_decanterMock`) was told to look in
|
|
96
|
+
`_decanterScenario.fill` — a key not in the file; the message now names the key
|
|
97
|
+
it actually found. A replay gap derives its node list from the **workflow
|
|
98
|
+
graph**, so those nodes are by definition *not* in `fill`, yet it said "see the
|
|
99
|
+
`_decanterScenario` block"; it now says they are **not** listed, and points at
|
|
100
|
+
`--extend`. And a node deliberately written as `"Node": []` was reported as
|
|
101
|
+
unfilled — it is now told apart from a node with no entry at all, with the
|
|
102
|
+
spelling for "emits nothing".
|
|
103
|
+
|
|
104
|
+
- **The agent guard's 401 no longer reads as "this project was never set up".**
|
|
105
|
+
It led with *"run `n8n-decanter init`"*, and a blind field-test round watched an
|
|
106
|
+
agent conclude from it that there was no `.env` and no token at all — then send
|
|
107
|
+
its user through a pointless `init`. The `.env` existed; the token had simply
|
|
108
|
+
been rotated. The guard now leads with the cause (*"n8n rejected decanter's
|
|
109
|
+
existing MCP credentials … they are configured but no longer valid"*), matching
|
|
110
|
+
what the CLI already said, and offers `init` only as the OAuth alternative. It
|
|
111
|
+
also maps **403** now, pointing at n8n → Settings → MCP.
|
|
112
|
+
|
|
113
|
+
- **A missing `typescript` is reported as a skipped check, not a failed one.**
|
|
114
|
+
`preflight`'s node-file typecheck needs `typescript` in your project. A
|
|
115
|
+
globally installed decanter ships none (it is a devDependency), and `init`
|
|
116
|
+
leaves an existing `package.json` alone — so scaffolding into a project you
|
|
117
|
+
already had produced a module-resolution stack trace surfacing as a *typecheck
|
|
118
|
+
failure*, which reads like a type error in your own code. It is now an honest
|
|
119
|
+
skip, named in the coverage block with the one-command fix
|
|
120
|
+
(`npm i -D typescript`).
|
|
121
|
+
|
|
122
|
+
- **The scaffolded `AGENTS.md` tells agents that `.env` is unreadable by policy
|
|
123
|
+
and that this is not evidence it is missing** — the reasoning trap behind the
|
|
124
|
+
401 finding above.
|
|
125
|
+
|
|
126
|
+
- **`node run` no longer answers a branch index with the wrong branch's data.**
|
|
127
|
+
`$('Node').all(1)`, `$items('Node', 1)` and `$input.all(1)` ask for a node's
|
|
128
|
+
*second* output — an `IF`'s false branch, a `Switch`'s other case. A fixture
|
|
129
|
+
pins **one** items array per node, so there is no honest answer, but the
|
|
130
|
+
argument was accepted and **ignored**: you got output 0's items and the node
|
|
131
|
+
looked like it worked, graded against a shape it will never see live. Worse
|
|
132
|
+
than empty data, because nothing fails. These calls now refuse with a message
|
|
133
|
+
naming the call and the two ways forward (pin that branch as its own fixture
|
|
134
|
+
node, or run it for real with `test`) — the same signpost pattern `$vars` and
|
|
135
|
+
`$secrets` already use. `n8n-globals.d.ts` declared the parameter and
|
|
136
|
+
`docs/cli/node-run.md` listed the calls as fully covered, so both surfaces had
|
|
137
|
+
promised something the emulation never did.
|
|
138
|
+
|
|
139
|
+
- **`pull` no longer destroys uncommitted local edits.** It committed the folder
|
|
140
|
+
*after* overwriting it, so an uncommitted `.js` edit was gone and had never
|
|
141
|
+
entered git — while the warning printed on that exact path told you to
|
|
142
|
+
"recover via git". Pull now takes a **snapshot commit before it writes
|
|
143
|
+
anything** (`watch` and the live mirror already did). If the snapshot cannot be
|
|
144
|
+
made — no git repo, `commitOnPull: false`, a git error — the pull still runs,
|
|
145
|
+
but the warning says the overwrite is **not** recoverable instead of promising
|
|
146
|
+
a safety net that isn't there.
|
|
147
|
+
|
|
148
|
+
- **`pull`'s clobber warning now fires for a node it has never synced.** It was
|
|
149
|
+
gated on the node already having a sync baseline, which is backwards on the
|
|
150
|
+
read side: no baseline means the node isn't in `.decanter.json` yet, so the
|
|
151
|
+
local file is precisely the one with no protection. The loss path this opened
|
|
152
|
+
matches the scaffolded agent workflow exactly — an agent adds a Code node over
|
|
153
|
+
the guard (the guard blocks `jsCode`, so the remote body is empty), writes the
|
|
154
|
+
source into `code/<node>.js`, and a background mirror pull lands before the
|
|
155
|
+
first push: fresh file replaced by the empty remote body, silently.
|
|
156
|
+
|
|
157
|
+
- **The live mirror stops refreshing when its safety commit fails.** It awaited
|
|
158
|
+
the commit and discarded the result, but that call returns a failure (it never
|
|
159
|
+
throws) for any git error — unset identity, a mid-merge tree, `index.lock`, a
|
|
160
|
+
rejecting hook. The documented "a dirty tree is safety-committed before the
|
|
161
|
+
pull" rail therefore degraded silently into an unrecoverable overwrite. It now
|
|
162
|
+
skips the refresh and says why, matching `watch`.
|
|
163
|
+
|
|
164
|
+
- **Scenario gaps are now judged per branch, not per node.** A branching node
|
|
165
|
+
(an `IF`, a `Switch`) emits on **one** output per run, but `preflight
|
|
166
|
+
--simulate` and `scenario create --execution` read its *first* output for every
|
|
167
|
+
outgoing edge. Two consequences, both wrong: a node on the branch the capture
|
|
168
|
+
**never took** was reported as a gap you had to write pin data for, and a
|
|
169
|
+
genuine gap's `inputSample` was filled from the **other** branch's items — so
|
|
170
|
+
you coded against the wrong upstream shape. Untaken branches are exempt and
|
|
171
|
+
neutralized, as documented.
|
|
172
|
+
|
|
173
|
+
- **An n8n whose MCP access is switched off now says so.** Turning MCP off is a
|
|
174
|
+
one-click state in n8n (Settings → MCP) and it makes the server answer a valid
|
|
175
|
+
token with `403 MCP access is disabled` — it never 404s. decanter recognised
|
|
176
|
+
401 and 404 but not 403, so every command failed with a bare
|
|
177
|
+
`403 Forbidden` and no next step. It now surfaces n8n's own reason plus the
|
|
178
|
+
fix. The 404 message was reworded to match what 404 actually means (no MCP
|
|
179
|
+
server at that address — wrong `N8N_HOST`, or an n8n too old to have one).
|
|
180
|
+
|
|
181
|
+
Note the ordering trap, now documented in the troubleshooting FAQ: a missing
|
|
182
|
+
or stale token still returns 401 while MCP is off, so the 401 can hide the 403.
|
|
183
|
+
|
|
184
|
+
## [0.9.0] - 2026-08-04
|
|
185
|
+
|
|
186
|
+
### Changed
|
|
187
|
+
|
|
188
|
+
- **The agent guard now refuses a `publish_workflow` that would take a broken
|
|
189
|
+
draft live.** `publish` already checked, but the raw MCP tool went straight
|
|
190
|
+
through the guard — so an agent could go live around the verb and ship exactly
|
|
191
|
+
the breakage the check exists to catch. Both transports (`mcp connect` and
|
|
192
|
+
`mcp serve`) run the same check on the same shared code.
|
|
193
|
+
|
|
194
|
+
**Fail-closed:** if the check itself cannot run — n8n unreachable — the publish
|
|
195
|
+
is refused too, and the message says the *check* failed rather than claiming
|
|
196
|
+
the workflow is broken. A read that fails almost certainly means the publish
|
|
197
|
+
would have failed anyway, and "couldn't verify, so we shipped it" is not a gate.
|
|
198
|
+
|
|
199
|
+
- **Dangling-reference checks now cover all four forms n8n rewrites on a rename**
|
|
200
|
+
— `` $('X') `` (as before) plus `$node["X"]`, `$node.X` and `$items('X')`.
|
|
201
|
+
Previously only the first was detected, so a rename could strand a `$node[…]`
|
|
202
|
+
call site that nothing reported: `preflight`, `push`, `test` and `publish` all
|
|
203
|
+
passed it, and it failed at run time instead. The rule is n8n's own — its
|
|
204
|
+
rewriter handles exactly these four — so if n8n treats it as a reference, the
|
|
205
|
+
guard now does too.
|
|
206
|
+
|
|
207
|
+
**This can surface errors in workflows that passed before.** A `$node["Old"]`
|
|
208
|
+
reference to a node that no longer exists is a hard compliance error, which
|
|
209
|
+
`--force` does not bypass. The message quotes the reference **as written**, so
|
|
210
|
+
it is clear which form triggered it. Computed references (`$(someVar)`, a
|
|
211
|
+
template literal with `${…}`) are still left alone — a regex cannot resolve
|
|
212
|
+
them, and n8n has the same limit.
|
|
213
|
+
|
|
214
|
+
- **Breaking: `n8n-decanter test <workflow>` no longer executes.** It used to
|
|
215
|
+
fall back to the newest capture under `executions/` and run the workflow for
|
|
216
|
+
real on your instance — a directory that is gitignored, so the same commit
|
|
217
|
+
behaved differently for different people, and a bare verb had real side
|
|
218
|
+
effects. Bare `test` is now a **static tier**: it reads the instance's draft,
|
|
219
|
+
reports dangling `$('…')` references, and runs nothing. Pass
|
|
220
|
+
`--execution <id>` or `--scenario <slug>` for the pinned run, which is
|
|
221
|
+
otherwise unchanged. There is no deprecation shim — a `test` that still
|
|
222
|
+
executed *sometimes* would keep exactly the ambiguity this removes.
|
|
223
|
+
|
|
224
|
+
The pinned run now also does the static check first, so a draft already known
|
|
225
|
+
to be broken is never fired at the instance.
|
|
226
|
+
- **`publish` refuses a draft carrying a dangling `$('…')` reference.**
|
|
227
|
+
Previously nothing checked: the compliance guard runs on `push`, `preflight`
|
|
228
|
+
and `backup` — not `publish` — so a task that only renamed nodes never hit a
|
|
229
|
+
gate and the break went live. The check reads **the draft on the instance**
|
|
230
|
+
(the read `publish` already makes), not your local folder: `workflow.json` is
|
|
231
|
+
a snapshot, so grading it would pass a broken workflow on a stale mirror and
|
|
232
|
+
block a legitimate publish from a fresh clone.
|
|
233
|
+
|
|
234
|
+
### Added
|
|
235
|
+
|
|
236
|
+
- **`init` now scaffolds a hook that catches stranded `$('…')` references right
|
|
237
|
+
after a rename.** n8n's `renameNode` MCP op rewrites the node name and the
|
|
238
|
+
connections only, so the references it leaves behind used to surface at the
|
|
239
|
+
next `push` — arbitrarily far from the rename that caused them. On Claude Code
|
|
240
|
+
a PostToolUse hook on `update_workflow` now reports them immediately, split
|
|
241
|
+
into the two halves and in the order they must be repaired: other nodes'
|
|
242
|
+
expression parameters in n8n first, then the code files here, then `push`.
|
|
243
|
+
|
|
244
|
+
It scans for the old name instead of running `preflight`, deliberately: the
|
|
245
|
+
hook fires before the background snapshot refresh, and until that lands the
|
|
246
|
+
snapshot still carries the old name, so every reference still resolves and
|
|
247
|
+
`preflight` would report clean. Silent when nothing references the renamed
|
|
248
|
+
node. The checklist in the scaffolded `AGENTS.md` remains the contract for
|
|
249
|
+
every agent — the hook is a reminder, not a replacement.
|
|
250
|
+
|
|
251
|
+
### Fixed
|
|
252
|
+
|
|
253
|
+
- **Corrected the rename guidance: n8n's `renameNode` MCP op does NOT rewrite
|
|
254
|
+
`$('…')` references.** The scaffolded agent guide (and the 0.6.0 notes below)
|
|
255
|
+
claimed n8n rewrites connections *and* `$('…')` references server-side on a
|
|
256
|
+
rename. Verified against real n8n 2.30.7 and 2.33.3: the MCP op rewrites the
|
|
257
|
+
node name and the **connections only**, then reports success with
|
|
258
|
+
`validationWarnings: []` — every `$('Old Name')` ref is left dangling, both in
|
|
259
|
+
Code-node source and in other nodes' expression parameters. The n8n *editor*
|
|
260
|
+
does rewrite them, but in the browser before it saves, so "server-side" was
|
|
261
|
+
wrong for that path too. No amount of pulling repairs this; `pull` faithfully
|
|
262
|
+
mirrors what n8n stored.
|
|
263
|
+
|
|
264
|
+
`init`'s `AGENTS.md`/`CLAUDE.md` now describe the real contract, including the
|
|
265
|
+
repair order that matters: **fix other nodes' expression parameters over MCP
|
|
266
|
+
first, then local code, then `push`.** The other order loses the code fix,
|
|
267
|
+
because a forwarded MCP write schedules a background snapshot refresh whose
|
|
268
|
+
pull overwrites unpushed `.js` edits.
|
|
269
|
+
- **A dangling-reference error now says which half it is and where to fix it.**
|
|
270
|
+
The two compliance errors were near-identical and neither mentioned a rename,
|
|
271
|
+
which led to `workflow.json` being hand-edited — turning the check green while
|
|
272
|
+
n8n stayed broken.
|
|
273
|
+
|
|
274
|
+
## [0.8.0] - 2026-07-27
|
|
275
|
+
|
|
276
|
+
### Added
|
|
277
|
+
|
|
278
|
+
- **The interactive picker offers a `--force` retry when a `push` hits the drift
|
|
279
|
+
guard.** Previously the error was printed and you were dropped back at the
|
|
280
|
+
menu, having to leave the picker and re-run `push --force` by hand — even
|
|
281
|
+
though the CLI had just told you `--force` would fix it. Now it asks:
|
|
282
|
+
|
|
283
|
+
```
|
|
284
|
+
✗ remote code changed since last sync — pull first (or repeat with --force to overwrite the draft)
|
|
285
|
+
retry with --force and overwrite the remote draft? [y/N]
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
The default is **No**: a bare `Enter`, or anything other than `y`/`yes`,
|
|
289
|
+
returns to the menu exactly as before. Answering `y` re-runs the same menu row
|
|
290
|
+
(flags included) with `--force`, overwriting the n8n **draft** only — the
|
|
291
|
+
published version is untouched.
|
|
292
|
+
|
|
293
|
+
**It only appears for failures `--force` can actually fix.** A layout
|
|
294
|
+
compliance error never prompts, because forcing does not bypass it. And this
|
|
295
|
+
is the interactive picker session only: piped and non-interactive runs never
|
|
296
|
+
prompt — they print the `--force` hint and exit non-zero, unchanged.
|
|
297
|
+
|
|
298
|
+
- **`diff` — the new verb for "show me the actual changed lines".** It is the
|
|
299
|
+
promoted half of `status --diff`: per-node unified line diffs of your local
|
|
300
|
+
code against the n8n draft, `.ts` compiled first (bundling `shared/*`, so a
|
|
301
|
+
helper edit shows every importing node). Nodes that are in sync are omitted
|
|
302
|
+
entirely, and a clean tree says so in one line. Multi-ref like `pull`/`push`;
|
|
303
|
+
no workflow on a terminal opens the picker.
|
|
304
|
+
|
|
305
|
+
**It always exits 0.** `diff` is an inspection view, like `git diff` — the
|
|
306
|
+
gate is `preflight`. See the migration note under Removed.
|
|
307
|
+
|
|
308
|
+
- **`preflight --viewer`** (with `--simulate`): leaves a browsable throwaway
|
|
309
|
+
n8n running so you can open the replayed run in the UI — the interactive half
|
|
310
|
+
of the old `simulate` verb, now explicit instead of implied by a TTY. It does
|
|
311
|
+
**not** relax preflight's safety contract: the graded run stays headless with
|
|
312
|
+
`--network-none`, and the viewer is a second, separate container. A workflow
|
|
313
|
+
with a multi-batch loop reports `simulate` as **skipped** under `--viewer`
|
|
314
|
+
("a preview, not a pass/fail check"), never as a pass.
|
|
315
|
+
|
|
316
|
+
- **`preflight --no-typecheck`** skips the `types` check — the escape hatch the
|
|
317
|
+
retired `check` verb had.
|
|
318
|
+
|
|
319
|
+
- **The agent guard now logs a startup line and an audit trail** (`mcp connect`
|
|
320
|
+
and `mcp serve` alike, on stderr):
|
|
321
|
+
|
|
322
|
+
```
|
|
323
|
+
guard: connected to <host> — forwarding all n8n MCP tools, blocking jsCode writes in update_workflow
|
|
324
|
+
guard: forwarded search_workflows
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
Previously the guard spoke **only** when it blocked something, so an empty log
|
|
328
|
+
meant either "ran, blocked nothing" or "never started" — indistinguishable,
|
|
329
|
+
and opposite in meaning. The startup line settles that; the per-call lines
|
|
330
|
+
make the guard the one place that can answer *what did an agent actually do
|
|
331
|
+
to my n8n instance?*, since every MCP call passes through it.
|
|
332
|
+
|
|
333
|
+
**Tool names only — arguments are never logged**, so the log stays safe to
|
|
334
|
+
attach to a bug report.
|
|
335
|
+
|
|
336
|
+
- **Every `preflight` finding can now carry `details[]`** — the full list behind
|
|
337
|
+
the one-line message: *every* layout violation, *every* `tsc` error, the
|
|
338
|
+
drifted node names, the viewer URL. Printed indented under the check line,
|
|
339
|
+
and present in `--json`. This is how the information `check` printed in full
|
|
340
|
+
survives its removal; without it, folding `check` into `preflight` would have
|
|
341
|
+
truncated a 12-violation layout failure to its first line.
|
|
342
|
+
|
|
343
|
+
### Changed
|
|
344
|
+
|
|
345
|
+
- **The picker lists pulled workflows newest-synced first**, instead of the
|
|
346
|
+
folder's alphabetical order — the workflow you last pulled or pushed is under
|
|
347
|
+
the cursor when the picker opens. Unpulled remote rows keep their place after
|
|
348
|
+
the local ones. The signal is each workflow folder's sync timestamp, so it is
|
|
349
|
+
*local activity*, not committed history: right after a fresh `git clone`
|
|
350
|
+
everything looks equally recent and the list falls back to alphabetical until
|
|
351
|
+
your first pull or push. Scripted `list` output is deliberately unchanged.
|
|
352
|
+
|
|
353
|
+
- **The CLI banner's `n8n` wordmark now uses the brand orange, matching the
|
|
354
|
+
website** (`#E18528`, derived from the site's accent color) rather than ANSI
|
|
355
|
+
red. It degrades gracefully — a 256-color terminal gets the nearest orange, a
|
|
356
|
+
16-color one keeps the old red — and piped output and `NO_COLOR` stay plain,
|
|
357
|
+
exactly as before.
|
|
358
|
+
|
|
359
|
+
- **Breaking: `preflight`'s profiles are replaced by two orthogonal flags.**
|
|
360
|
+
`--full` and the `Profile` model are gone. Depth is now `--simulate`
|
|
361
|
+
(**additive** — appends the local-engine run of your code) and `--offline`
|
|
362
|
+
(**subtractive** — drops the instance-reads tier), and they compose:
|
|
363
|
+
|
|
364
|
+
```
|
|
365
|
+
preflight static + instance reads (the default gate)
|
|
366
|
+
preflight --simulate + a local-engine run of your code
|
|
367
|
+
preflight --offline static only — no instance contact
|
|
368
|
+
preflight --offline --simulate static + local engine, no instance
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
**Migration:** `--full` → `--simulate`. And read the next entry carefully —
|
|
372
|
+
`--offline` still exists but means something narrower.
|
|
373
|
+
|
|
374
|
+
- **Breaking: `preflight --offline` no longer runs the local-engine replay.**
|
|
375
|
+
It used to mean "static + engine, no instance"; it now means "static only".
|
|
376
|
+
The flag name is unchanged, so **nothing will error** — an air-gapped CI job
|
|
377
|
+
on `preflight --offline` simply stops running the engine and quietly loses
|
|
378
|
+
that coverage. **Migration:** `preflight --offline --simulate` is the old
|
|
379
|
+
`--offline`. (This narrowing is also what makes `--offline` fast enough for
|
|
380
|
+
the per-edit hook: it now spawns no Docker container.)
|
|
381
|
+
|
|
382
|
+
- **Breaking: `preflight --json` replaces `profile` with `flags`.** Where the
|
|
383
|
+
report carried `"profile": "default" | "full" | "offline"` it now carries
|
|
384
|
+
`"flags": {"simulate": false, "offline": false}`. Agents key on this. Every
|
|
385
|
+
other field is unchanged, and each entry in `checks[]` gains an optional
|
|
386
|
+
`details: string[]`.
|
|
387
|
+
|
|
388
|
+
- **`preflight` with no workflow and an empty `"workflows"` config now checks
|
|
389
|
+
every *pulled* workflow** instead of erroring with "no workflow ids" — the
|
|
390
|
+
behaviour the `check` verb had, kept now that `preflight` absorbs it.
|
|
391
|
+
|
|
392
|
+
- **A workflow folder with an unreadable `.decanter.json` no longer fails your
|
|
393
|
+
gate.** `check` scanned *folders*, so a corrupt state file anywhere under
|
|
394
|
+
`workflows/` was a hard error for the whole run. `preflight` grades resolved
|
|
395
|
+
*workflows*, and a folder whose state won't parse can't resolve to one — so
|
|
396
|
+
it is named in a warning (`corrupt .decanter.json (…) — skipping this
|
|
397
|
+
folder`) and skipped, while every healthy workflow is still graded. The fact
|
|
398
|
+
is still reported; it just no longer blocks work on unrelated workflows.
|
|
399
|
+
|
|
400
|
+
- **`preflight --simulate` accepts multiple workflows.** The old `simulate`
|
|
401
|
+
verb took exactly one; preflight loops, so a multi-ref run spins one engine
|
|
402
|
+
container per workflow, serially.
|
|
403
|
+
|
|
404
|
+
- **The scaffolded template now runs `preflight --offline` where it ran
|
|
405
|
+
`check`** — the PostToolUse verify hook, both `package.json` scripts, and the
|
|
406
|
+
agent-facing prose in `AGENTS.md` / `CLAUDE.md`. *Existing sync dirs keep
|
|
407
|
+
their files*: re-run `n8n-decanter init` to be offered the refresh, and note
|
|
408
|
+
that init leaves locally-modified files alone, so a hand-edited hook or
|
|
409
|
+
`package.json` still invokes a removed verb until you update it yourself.
|
|
410
|
+
|
|
411
|
+
- The scaffolded agent permission allowlist swaps its `check`/`status`/
|
|
412
|
+
`simulate` rules for a `diff` pair (both the bare and `npx` shapes);
|
|
413
|
+
`preflight --simulate` is already covered by the existing `preflight:*` rule.
|
|
414
|
+
|
|
415
|
+
- The interactive picker's action menu is now `preflight`, `preflight
|
|
416
|
+
--simulate`, `diff`, `pull`, `push`, `watch`, `executions` — a menu row may
|
|
417
|
+
carry flags, which is how the browsable local-engine run survives the fold.
|
|
418
|
+
|
|
419
|
+
- **The scaffolded agent contract now treats `push` as part of finishing the
|
|
420
|
+
work, and reserves "ask the user first" for `publish`.** A push lands on the
|
|
421
|
+
workflow's **draft** and never changes what is running; only `publish` /
|
|
422
|
+
`push --publish` / `unpublish` do. The old rule gated both behind "only when
|
|
423
|
+
the user asks", so an agent handed "build me an hourly job that tags orders"
|
|
424
|
+
would build the structure in n8n, write and verify all the Code, and then
|
|
425
|
+
**stop** — leaving every Code node empty on the instance and the real code in
|
|
426
|
+
the repo, reporting "ready to push". Correct by the old rule, and not what
|
|
427
|
+
anybody asked for. Agents are now told to push once offline checks pass, to
|
|
428
|
+
say what landed, and to still ask first when the workflow is published/active
|
|
429
|
+
or a teammate is editing it. Going live remains a deliberate, user-requested
|
|
430
|
+
step. Affects `template/AGENTS.md.example` (copied into new sync dirs by
|
|
431
|
+
`init`) and the `/docs/agents` surfaces. *(Surfaced by the Plan 35 blind field
|
|
432
|
+
test, where the "failing" agent was following the old contract exactly.)*
|
|
433
|
+
|
|
434
|
+
- **The scaffolded agent contract now follows the `preflight → push → test →
|
|
435
|
+
publish` flow.** `preflight` is local-only (it no longer runs on the instance),
|
|
436
|
+
so `test` — which runs the workflow's **draft** — is only meaningful *after* a
|
|
437
|
+
push. The old contract framed `test` as a pre-push runtime check and ended its
|
|
438
|
+
loop at `preflight → push`; both are now reconciled to the new order. Affects
|
|
439
|
+
`template/AGENTS.md.example` and the `/docs/agents` surfaces. *(Same surface
|
|
440
|
+
the Plan 60 verb reorder changed — kept in lockstep so the blind field test
|
|
441
|
+
grades agents against a contract that matches the tool.)*
|
|
442
|
+
|
|
443
|
+
- **Breaking: `preflight` no longer runs the instance-side `test` stage.** It
|
|
444
|
+
ran `test_workflow` against n8n's **draft**, while every other stage graded
|
|
445
|
+
your **local files** — so whenever a push was pending, one score described
|
|
446
|
+
two different versions of the workflow, flagged only by a `-10` parity warn.
|
|
447
|
+
A report could read *caution, 90/100* while its runtime evidence was about
|
|
448
|
+
code you weren't shipping. `preflight` now grades local code only; the
|
|
449
|
+
instance is read for sync facts and never executed.
|
|
450
|
+
|
|
451
|
+
The documented flow is **`preflight` → `push` → `test` → `publish`**: verify
|
|
452
|
+
local code, make it the draft, run what you actually pushed, then go live.
|
|
453
|
+
Nothing was removed from the toolbox — the instance run moved to where it
|
|
454
|
+
means something.
|
|
455
|
+
|
|
456
|
+
**Migration:** a CI job that gates on a plain `preflight` no
|
|
457
|
+
longer gets an instance run inside that gate — the draft is never executed by
|
|
458
|
+
preflight. To keep instance-run coverage, add `n8n-decanter test` as its own
|
|
459
|
+
step **after** your push step (`--require=test` users get a hard error with
|
|
460
|
+
this guidance; default-profile users get this note).
|
|
461
|
+
|
|
462
|
+
- **Breaking: `preflight --require=test` is rejected**, with a message pointing
|
|
463
|
+
at the new flow rather than a bare "unknown check". The `test` id is gone
|
|
464
|
+
from `--require`, from `--json` `checks[]`, and from `coverage`.
|
|
465
|
+
|
|
466
|
+
- **`preflight` auto-fetches a capture only under `--simulate`** — the flag
|
|
467
|
+
that adds the one stage which consumes a capture. Without it there is no
|
|
468
|
+
runtime stage, so preflight no longer fetches, and a missing or stale capture
|
|
469
|
+
is reported as `info` rather than `warn` — nothing would consume one
|
|
470
|
+
(`--offline` never reaches the instance to fetch either way).
|
|
471
|
+
|
|
472
|
+
- The `parity` warn is reworded. It was a caveat about the runtime tier
|
|
473
|
+
grading the wrong artifact; that's no longer possible, so it is now the plain
|
|
474
|
+
next step: *"local code differs from the draft in N node(s) — push to make it
|
|
475
|
+
the draft, then test"*.
|
|
476
|
+
|
|
477
|
+
- **The local-engine replay is the sole runtime stage** (`preflight --simulate`)
|
|
478
|
+
and needs Docker. For runtime evidence without Docker, push and then run
|
|
479
|
+
`test`.
|
|
480
|
+
|
|
481
|
+
- **Node-file type checking moved off TypeScript's legacy `node10` module
|
|
482
|
+
resolution** to `moduleResolution: "bundler"` (with `module: "preserve"`), in
|
|
483
|
+
the scaffolded `tsconfig.json`. This matches what push actually does — `.ts`
|
|
484
|
+
nodes are compiled with esbuild in bundling mode — and keeps the documented
|
|
485
|
+
extensionless import style working (`import { total } from
|
|
486
|
+
"../../shared/money"`). It also unblocks TypeScript 6, which turns `node10`
|
|
487
|
+
into a hard error (`TS5107`), and TypeScript 7, which removes it outright.
|
|
488
|
+
`node16`/`nodenext` were **not** chosen: they reject extensionless relative
|
|
489
|
+
imports (`TS2835`) and would force every node file to be rewritten with `.js`
|
|
490
|
+
extensions. No change to which Node.js versions are supported. *Existing sync
|
|
491
|
+
dirs keep their current `tsconfig.json`* — re-run `n8n-decanter init` to be
|
|
492
|
+
offered the refresh; if you have hand-edited yours (or created it before
|
|
493
|
+
template baselines existed), init reports it and leaves it alone, so apply
|
|
494
|
+
the two-line change yourself when you move to TypeScript 6+.
|
|
495
|
+
|
|
496
|
+
### Removed
|
|
497
|
+
|
|
498
|
+
- **Breaking: the `check`, `status`, and `simulate` verbs.** All three were
|
|
499
|
+
variations on "check my thing", and telling them apart was the single most
|
|
500
|
+
confusing part of the surface. They fold into two:
|
|
501
|
+
|
|
502
|
+
| You used to run | Now run |
|
|
503
|
+
| --- | --- |
|
|
504
|
+
| `check [workflow…]` | `preflight --offline [workflow…]` |
|
|
505
|
+
| `check --no-typecheck` | `preflight --offline --no-typecheck` |
|
|
506
|
+
| `status [workflow…]` | `preflight [workflow…]` (the scored summary) |
|
|
507
|
+
| `status --diff` | `diff [workflow…]` (the changed lines) |
|
|
508
|
+
| `simulate <workflow>` | `preflight <workflow> --offline --simulate` |
|
|
509
|
+
| `simulate --network-none` | `preflight --simulate` (always network-none) |
|
|
510
|
+
|
|
511
|
+
Each removed verb exits non-zero naming its replacement, so a stale script
|
|
512
|
+
fails loudly rather than silently. **Note the `simulate` row:** the verb
|
|
513
|
+
needed no credentials, and a bare `preflight --simulate` still runs the
|
|
514
|
+
instance tier — add `--offline` for the credential-free equivalent.
|
|
515
|
+
|
|
516
|
+
Nothing was lost with the verbs. The compliance guard still gates every push
|
|
517
|
+
and watch save (only the standalone *view* is gone, and preflight's `layout`
|
|
518
|
+
finding now lists every violation); the publish state, live-lags-draft note
|
|
519
|
+
and snapshot-stale hint are preflight's `lifecycle` and `snapshot` findings.
|
|
520
|
+
|
|
521
|
+
- **Breaking: `status`'s CI exit codes.** `status` exited 1 on a code conflict
|
|
522
|
+
or remote drift, and pipelines gated on that. `diff` always exits 0.
|
|
523
|
+
**Migration:** gate on `preflight`, whose `drift` check **fails** on a
|
|
524
|
+
CONFLICT and warns on remote drift (add `--fail-on=warn` to gate on the warn
|
|
525
|
+
too).
|
|
526
|
+
|
|
527
|
+
- **`simulate --network-none`.** `preflight` always runs the graded engine
|
|
528
|
+
replay with no network, so the flag had nothing left to turn off.
|
|
529
|
+
|
|
530
|
+
- **Breaking: the `preflight --quick` profile.** With the `test` stage gone it
|
|
531
|
+
was byte-identical to the default profile, and rather than redefine it into a
|
|
532
|
+
meaning users would have to learn and then unlearn, it is gone. Static-only
|
|
533
|
+
checking is now `preflight --offline`; the local-engine replay is
|
|
534
|
+
`preflight --simulate` (see the profile→flags entry above, which retired
|
|
535
|
+
`--full` and the whole profile vocabulary in the same release).
|
|
536
|
+
|
|
537
|
+
**Neither `--quick` nor `--full` is recognized any more — they are simply
|
|
538
|
+
gone, not rejected with a migration.** The CLI ignores flags it does not
|
|
539
|
+
know, so `preflight --full` now runs the *default* gate (no engine) and
|
|
540
|
+
`preflight --quick` runs it too, both exiting 0. **If you have either in a CI
|
|
541
|
+
job, update it in the same step as this upgrade** — nothing will tell you at
|
|
542
|
+
runtime. `--full` → `--simulate`; `--quick` → `--offline`.
|
|
543
|
+
- **Breaking: `preflight --trigger <node>`.** It existed only to feed the
|
|
544
|
+
removed instance `test` stage; since that stage's removal it parsed and did
|
|
545
|
+
nothing. `test --trigger <node>` (the post-push instance run) keeps the flag
|
|
546
|
+
— that is where trigger selection acts.
|
|
547
|
+
|
|
548
|
+
### Fixed
|
|
549
|
+
|
|
550
|
+
- **Renaming a `.ts` node in n8n no longer leaves it stuck on "push pending".**
|
|
551
|
+
A `.ts` node that imports from `shared/` (or an opted-in npm package) is
|
|
552
|
+
compiled with esbuild's bundler, and esbuild labels every bundled module with
|
|
553
|
+
a `// <path>` comment. That label used the node's **own filename**, so it
|
|
554
|
+
landed inside the compiled bytes — and inside the `@ts-n8n sha256:` marker.
|
|
555
|
+
Rename the node in n8n, `pull` renames `compute.ts` → `ümläut-nödé.ts`, and
|
|
556
|
+
the artifact changed even though **not one line of your source did**: `diff`
|
|
557
|
+
reported a difference that was purely the comment line, and the node read
|
|
558
|
+
"local changes — push pending" until you pushed a no-op. The entry label is
|
|
559
|
+
now a fixed name, so a pure rename round-trip is byte-stable and comes back
|
|
560
|
+
clean.
|
|
561
|
+
|
|
562
|
+
**One-time effect when you upgrade:** because the compiled bytes changed, the
|
|
563
|
+
first `diff`/`pull` after upgrading reports **"modified, not yet pushed"** for
|
|
564
|
+
every `.ts` node **that has imports** — a comment-line difference only. One
|
|
565
|
+
`push` per workflow clears it, and it is a plain push, not a conflict: the
|
|
566
|
+
remote code is untouched, so the drift guard does not trip and `--force` is
|
|
567
|
+
not needed. `.ts` nodes **without** imports compile through a different path
|
|
568
|
+
that never embedded the name, and `.js` nodes are unaffected.
|
|
569
|
+
|
|
570
|
+
- **The scaffolded MCP guard now starts under a *local* install, not only a
|
|
571
|
+
global one.** `init`'s `.mcp.json` / `opencode.json` spawned the guard as a
|
|
572
|
+
bare `n8n-decanter mcp connect`, which only resolves when the CLI is on the
|
|
573
|
+
agent's `PATH` — i.e. a **global** install. With decanter installed as a
|
|
574
|
+
**local** project dependency the command silently failed to start, so the
|
|
575
|
+
agent got no guarded route and fell back to whatever other n8n MCP it had,
|
|
576
|
+
unguarded. The scaffolded command is now `npx --no-install n8n-decanter mcp
|
|
577
|
+
connect`, which resolves the local `node_modules` bin **and** a global
|
|
578
|
+
install; `--no-install` never downloads from npm, so a genuinely missing
|
|
579
|
+
install fails loudly instead of silently. (Plan 58.)
|
|
580
|
+
|
|
581
|
+
- **The scaffolded agent permissions now cover `npx n8n-decanter …` — including
|
|
582
|
+
the `push --force` denial.** The same local-install gap applies to the CLI
|
|
583
|
+
calls an agent makes in a shell: under a local (devDependency) install a bare
|
|
584
|
+
`n8n-decanter <verb>` is not on `PATH`, so the working form is
|
|
585
|
+
`npx n8n-decanter <verb>`. The permission matcher keys on the command prefix,
|
|
586
|
+
so that form previously matched **neither** the allow rules (every safe call
|
|
587
|
+
would stop to ask) **nor the `push --force` deny rule** — meaning the
|
|
588
|
+
force-push guard rail could be sidestepped simply by invoking through `npx`.
|
|
589
|
+
Both lists now carry the `npx` forms (Claude Code `settings.json` and
|
|
590
|
+
opencode), and the scaffolded `AGENTS.md` tells agents to add the prefix when
|
|
591
|
+
the bare command is not found. Installing globally is **not** required — a
|
|
592
|
+
per-sync-dir devDependency remains fully supported. (Plan 58.)
|
|
593
|
+
|
|
10
594
|
## [0.7.0] - 2026-07-24
|
|
11
595
|
|
|
12
596
|
### Added
|