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.
Files changed (42) hide show
  1. package/CHANGELOG.md +584 -0
  2. package/README.md +46 -74
  3. package/dist/lib/api.mjs +35 -0
  4. package/dist/lib/compile.mjs +11 -2
  5. package/dist/lib/config.mjs +12 -1
  6. package/dist/lib/diff.mjs +75 -3
  7. package/dist/lib/engine.mjs +3 -2
  8. package/dist/lib/errors.mjs +20 -0
  9. package/dist/lib/executions.mjs +5 -5
  10. package/dist/lib/init.mjs +9 -4
  11. package/dist/lib/lifecycle.mjs +16 -0
  12. package/dist/lib/mcp.mjs +23 -3
  13. package/dist/lib/mcpconnect.mjs +32 -4
  14. package/dist/lib/mcpserve.mjs +90 -2
  15. package/dist/lib/mirror.mjs +13 -2
  16. package/dist/lib/picker.mjs +95 -2
  17. package/dist/lib/preflight.mjs +162 -73
  18. package/dist/lib/prompt.mjs +3 -3
  19. package/dist/lib/pull.mjs +29 -6
  20. package/dist/lib/push.mjs +8 -4
  21. package/dist/lib/run.mjs +36 -5
  22. package/dist/lib/simulate.mjs +210 -29
  23. package/dist/lib/state.mjs +11 -2
  24. package/dist/lib/status.mjs +17 -87
  25. package/dist/lib/style.mjs +44 -1
  26. package/dist/lib/testrun.mjs +54 -20
  27. package/dist/lib/util.mjs +48 -23
  28. package/dist/lib/validate.mjs +174 -26
  29. package/dist/n8n-decanter.mjs +209 -214
  30. package/dist/scripts/typecheck.mjs +15 -1
  31. package/package.json +4 -4
  32. package/template/.claude/hooks/mcp-route-check.mjs.example +1 -1
  33. package/template/.claude/hooks/rename-refs.mjs.example +126 -0
  34. package/template/.claude/hooks/verify.mjs.example +26 -8
  35. package/template/.claude/settings.json.example +30 -7
  36. package/template/.mcp.json.example +2 -2
  37. package/template/AGENTS.md.example +193 -91
  38. package/template/CLAUDE.md.example +11 -7
  39. package/template/decanter-ts-plugin/index.js.example +1 -1
  40. package/template/opencode.json.example +5 -2
  41. package/template/package.json.example +2 -2
  42. 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