v0.10.1
Added
-
The live mirror now tells the agent when it overwrote your work. It runs a
fullpullafter a structure edit, so it can replace an unpushed local code
edit with what is on the instance. It always warned about that — on stderr,
which is the one stream an MCP agent structurally cannot read, so the party
able to react never heard it. The warning now rides the result of the
agent's next tool call, naming the files and how to recover them from the
safety commit. Delivered once, never repeated.Only on
mcp connect(the transportinitscaffolds).mcp servepipes
upstream responses through untouched — including SSE — and buffering them to
inject an advisory line would break streaming for every response to deliver it
on some. On that transport the stderr warning stays the only signal. -
initnow says that its permission rules only bind the next session. It
writes.claude/settings.jsonwith the deny rules that keep an agent off
.decanter.json,.envandpush --force— but agents read permission config
at startup, not on change, andinitis normally run from inside the session
those rules are meant to constrain. They were silently inert until a restart,
and the docs mentioned a restart only for the skills plugin — so the rules that
actually gate the agent went unmentioned. Printed once, when the file is first
written; a re-init in a set-up directory stays quiet. -
node runfixtures can pin a node's other outputs, so$('Node').all(1)
finally answers. Give a node one items array per output and the branch
is readable offline:"nodes": { "Decide": [[{ "json": { "side": "true" } }], [{ "json": { "side": "false" } }]] }
all(1)/first(1)/last(1)and$items('Decide', 1)read output 1, and
an empty array is a real answer — that branch took no items.inputtakes
the same shape, indexed by the node's input (a Merge node's second input).
A plain items array still means a single output, so existing fixtures are
unchanged; asking for an output the fixture doesn't supply still refuses,
now saying how many it has. Until now every such call was refused outright,
because a fixture could only express one array per node. -
preflight --simulatenow replays a pinned node's other outputs too. The
stand-in decanter substitutes for a network node is a Code node, which has one
output — so an error output (or any second branch) captured in your execution
was replayed nowhere, and everything behind it sat with no input, emitted
nothing, and let the run pass. Each populated output now gets its own
stand-in, wired to the same input as the original and feeding exactly that
output's targets, so the branch really runs. Deliberately not wired to the
synthetic trigger: a stand-in fires only when the original would, so a replay
whose real nodes take a different path can't have the old branch's items
injected into it anyway. Thesimulatecheck names the splits in its details
(terminal and--json).teststill replaysmain[0]only — n8n's
pinDatais one flat items array per node, with no output dimension — and
scenario checknow spells out which of the two you are looking at. -
testnow reports what the run actually moved, not just that it
finished — a coverage line over the nodes that executed (enabled and
unpinned; a pinned node's items are the input you supplied):
coverage: 7/9 unpinned node(s) emitted items — 2 emitted none: Group products, Write rows. A node counts as emitting if it put an item on any
output. Some empty nodes are normal — a filter that dropped everything — so
the line warns and nothing more.But a run in which not one unpinned node emitted an item now fails
(exit 1), even with synthetic pins. n8n calls such a runsuccessand
it is: nothing errored. No data moved either, so nothing was demonstrated,
and reporting it as a pass was the check lying. The message names the usual
cause — a pin replays a node's first output only. -
scenario checkwarns about what the replay will throw away. Both replay
paths (test'spinData,preflight --simulate's stand-in node) read
main[0]only, while the validator happily accepts — and✓ valids — a
scenario carrying items on several outputs. The check now says so offline:
once for a node whose data populates more than one output, naming the indices
that get dropped, and once for a node source that reads a pinned node's
non-first output ($('Enrich').all(1),$items('Enrich', 1)) — the call that
returns nothing and leaves the node emitting nothing. Warnings only; the
scenario stays valid for the outputs that do replay.
Changed
- An explicit
.tsextension in a node file's import no longer fails the
typecheck. The scaffoldedtsconfig.jsonnow sets
allowImportingTsExtensions, soimport { total } from "../../../shared/money.ts"type-checks — until now it was rejected
(TS5097) even thoughpushbundles it without complaint, which made the
gate and the bundler disagree over a pure spelling choice. Extensionless
stays the recommended form (it survives a helper later becoming.js);
both spellings resolve everywhere. Existing sync dirs are offered the
updatedtsconfig.jsonon the nextinit— atsconfig.jsonyou edited
yourself is reported as drift and left alone, so add the option by hand
there. - Two of the four import rules for
.tsnodes now warn instead of
blocking a push: a relative import resolving outside the sync dir, and an
absolute-path import. Both only endanger the author's own portability — the
bundle still builds locally and fails loudly (Could not resolve) wherever
the target is genuinely absent — so blocking them was decanter making the
user's call. The advisory prints on every surface (preflight'slayout
details,push,node run) and exactly once per push;
preflight --fail-on=warnis the strict variant for CI. Node builtins
and npm packages not opted intobundleDependenciesstill block —
esbuild is silent about both, so without the block the failure would
surface at runtime on the n8n instance. - The scaffolded
mcp-route-check.mjssession hook now also inspects
user-level agent config for direct n8n MCP routes — Claude Code's
~/.claude.json(including its entry for the current project), Cursor's
~/.cursor/mcp.json, the VS Code user profile, and opencode's global
config. Previously it read only project files, so ann8nserver added
withclaude mcp add -s user(or any other user-scoped config) bypassed
the decanter guard without a word — exactly the "second door" the hook
exists to catch. Still a warning, never a gate. Re-runinitin an
existing sync dir (or re-copy the hook from the template) to pick it up. - The scaffolded
tsconfig.jsonnow covers the whole sync dir, not just
shared/andworkflows/— helper code may live in any folder inside the
sync dir (shared/is only the scaffolded default), so the typecheck and
the editor's tsserver now own every root without a config edit. Existing
sync dirs keep their scaffolded file; to match, widenincludeto
["n8n-globals.d.ts", "**/*.ts", "**/*.js"]and add
"**/backups/**", "**/executions/**", "decanter-ts-plugin", "dist"to
exclude. Two consequences worth knowing: a loose node-shaped scratch file
(top-levelreturnoutside any workflow'scode/) is now part of the
program and reports TS1108 — move it into a workflow or add its folder to
exclude; and wheninitscaffolds into an existing project that had
notsconfig.json, the new config sweeps that project's own.ts/.js
into the node-file typecheck — add your app dirs toexcludeif they
shouldn't gate pushes.
Fixed
-
diffandpreflightno longer report aCONFLICTfor a node with no
recorded sync hash. "Changed both locally and remotely" is measured against
the last-sync baseline in.decanter.json; with no baseline nothing is
known to have moved on the instance, andpushhas always treated that as
pushable. The two disagreed, so the report described a dead end the CLI did
not have — worse, its documented exit (push --force) is denied to agents by
the permission rulesinitscaffolds. Such a node now reads as
push pending, which is whatpushdoes with it.pullno longer warns
CONFLICTfor the same case on.tsnodes. -
A
.js→.tsconversion no longer reads as data loss. Re-pointing a
node's//@file:placeholder is the sanctioned way to convert, andpush
andpullboth adopt it before doing anything — butdiffandpreflight
looked the file up in.decanter.jsonalone and announcedlocal file code/<node>.js missingfor the file you had just replaced. They now read the
placeholder too, so a converted node reportslocal changes in code/<node>.ts — push pending, identically for every converted node. -
preflightno longer prints✓ parity local code matches the draft
directly above✗ drift CONFLICT. Both checks read the same facts, so
paritymay claim a match only when every node is in sync; divergence that
driftowns is reported as aninfoline pointing at it. -
mcp connectsurvives an unreachable n8n instead of dying at the
handshake.initializewas forwarded like any other message, so a
connection failure answered the handshake with an error: the agent's MCP
client got noserverInfoand tore the session down before a single tool
call could report what was wrong. The guard now completes the handshake
itself when n8n does not answer, and the failure surfaces on the tool call
that needed the instance. Once n8n is reachable, the handshake is replayed
upstream so the session it uses is a real one. The startup line now reads
guard: ready — forwarding all n8n MCP tools to <host>instead of
connected to <host>, which claimed a connection nothing had made yet. -
The scaffolded
AGENTS.mdnow names the n8n-side prerequisites. MCP
access is a switch per instance and one per workflow ("Available in
MCP"), and until a workflow's is on,pull/push/diff/preflight/watch
all fail for it. The CLI's error says so; the file an agent reads before
running anything did not. -
preflight'stypescheck now reports type errors in shared helper
files instead of passing green whilepushfails on them. The scoped
typecheck dropped every diagnostic outside the graded workflow's own dir —
and helper code lives outside every workflow dir by definition — so a
workflow could gradereadyon codepushthen rejected. Helper
diagnostics (undershared/or any other folder) now surface in every
workflow'stypesline, while another workflow's node errors stay out of
scope as before. -
The documented shared-import path was one level short for the
code/
layout:../../shared/moneyin the TypeScript-nodes docs and the scaffolded
AGENTS.mdresolved toworkflows/shared/moneyand failed on copy-paste.
The correct depth fromworkflows/<folder>/code/is../../../shared/money. -
Compiled module labels — and therefore sync hashes — are now stable when
the sync dir is reached through a symlinked path (macOS/tmp, a
symlinked checkout). esbuild resolves bundled files to their realpaths, so
an un-realpathed label base produced machine-specific../…-climbing
labels inside the hashed bytes — nodes reading "push pending" across
machines with nobody touching code. The same spelling mismatch made a
scoped typecheck silently drop every node diagnostic under a symlinked
path; both now realpath. If a sync dir lives behind a symlink, the first
push after this release re-baselines the affected nodes' hashes once. -
The route-check hook now reads opencode's real config shape
(mcp.<name>). It previously looked only formcpServers/mcp.servers/
serversbags, so anopencode.jsonrouting straight at the instance was
listed as checked but never actually flagged. -
A
watchsave now runs the same folder-wide compliance guard aspush. It
used to check only the saved file, which cannot see the workflow's node names —
so it could not catch a dangling$('Renamed Node')and pushed straight to the
draft what a manualpushrefuses outright. The break then surfaced at run time
in n8n instead of at save time. Four surfaces already claimed the guards were
the same; now they are.The abort is scoped, deliberately: a save is refused when the violation is
in the file you just saved, and violations elsewhere in the folder are printed
on every save without blocking it. Repairing a rename means fixing several
files, and a folder-wide abort would stop every save until the last fix — which
would disablewatchduring exactly the job it is for. -
liveMirroris documented as what it is: a fullpull. The shipped agent
contract and the config reference both described it as refreshing the
workflow.jsonsnapshot. It also rewrites thecode/files and
.decanter.jsonand moves files on a rename, so it can overwrite an unpushed
local edit — which is worth knowing before you restructure. No behaviour change;
the docs now say what the code has always done.