Skip to content

v0.10.1

Choose a tag to compare

@buttjer buttjer released this 17 Aug 08:36
· 50 commits to main since this release
4c18c61

Added

  • The live mirror now tells the agent when it overwrote your work. It runs a
    full pull after 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 transport init scaffolds). mcp serve pipes
    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.

  • init now says that its permission rules only bind the next session. It
    writes .claude/settings.json with the deny rules that keep an agent off
    .decanter.json, .env and push --force — but agents read permission config
    at startup, not on change, and init is 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 run fixtures 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. input takes
    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 --simulate now 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. The simulate check names the splits in its details
    (terminal and --json). test still replays main[0] only — n8n's
    pinData is one flat items array per node, with no output dimension — and
    scenario check now spells out which of the two you are looking at.

  • test now 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 run success and
    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 check warns about what the replay will throw away. Both replay
    paths (test's pinData, 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 .ts extension in a node file's import no longer fails the
    typecheck.
    The scaffolded tsconfig.json now sets
    allowImportingTsExtensions, so import { total } from "../../../shared/money.ts" type-checks — until now it was rejected
    (TS5097) even though push bundles 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
    updated tsconfig.json on the next init — a tsconfig.json you edited
    yourself is reported as drift and left alone, so add the option by hand
    there.
  • Two of the four import rules for .ts nodes 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's layout
    details, push, node run) and exactly once per push;
    preflight --fail-on=warn is the strict variant for CI. Node builtins
    and npm packages not opted into bundleDependencies still 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.mjs session 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 an n8n server added
    with claude 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-run init in an
    existing sync dir (or re-copy the hook from the template) to pick it up.
  • The scaffolded tsconfig.json now covers the whole sync dir, not just
    shared/ and workflows/ — 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, widen include to
    ["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-level return outside any workflow's code/) is now part of the
    program and reports TS1108 — move it into a workflow or add its folder to
    exclude; and when init scaffolds into an existing project that had
    no tsconfig.json, the new config sweeps that project's own .ts/.js
    into the node-file typecheck — add your app dirs to exclude if they
    shouldn't gate pushes.

Fixed

  • diff and preflight no longer report a CONFLICT for 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, and push has 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 rules init scaffolds. Such a node now reads as
    push pending, which is what push does with it. pull no longer warns
    CONFLICT for the same case on .ts nodes.

  • A .js.ts conversion no longer reads as data loss. Re-pointing a
    node's //@file: placeholder is the sanctioned way to convert, and push
    and pull both adopt it before doing anything — but diff and preflight
    looked the file up in .decanter.json alone and announced local file code/<node>.js missing for the file you had just replaced. They now read the
    placeholder too, so a converted node reports local changes in code/<node>.ts — push pending, identically for every converted node.

  • preflight no longer prints ✓ parity local code matches the draft
    directly above ✗ drift CONFLICT.
    Both checks read the same facts, so
    parity may claim a match only when every node is in sync; divergence that
    drift owns is reported as an info line pointing at it.

  • mcp connect survives an unreachable n8n instead of dying at the
    handshake.
    initialize was forwarded like any other message, so a
    connection failure answered the handshake with an error: the agent's MCP
    client got no serverInfo and 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.md now 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's types check now reports type errors in shared helper
    files
    instead of passing green while push fails 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 grade ready on code push then rejected. Helper
    diagnostics (under shared/ or any other folder) now surface in every
    workflow's types line, 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/money in the TypeScript-nodes docs and the scaffolded
    AGENTS.md resolved to workflows/shared/money and failed on copy-paste.
    The correct depth from workflows/<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 for mcpServers / mcp.servers /
    servers bags, so an opencode.json routing straight at the instance was
    listed as checked but never actually flagged.

  • A watch save now runs the same folder-wide compliance guard as push. 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 manual push refuses 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 disable watch during exactly the job it is for.

  • liveMirror is documented as what it is: a full pull. The shipped agent
    contract and the config reference both described it as refreshing the
    workflow.json snapshot. It also rewrites the code/ files and
    .decanter.json and 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.