From febec321317268634e710c86e5bf826f39d28829 Mon Sep 17 00:00:00 2001
From: Felipe Domingues <53182096+domfelipe@users.noreply.github.com>
Date: Wed, 22 Jul 2026 11:43:41 -0300
Subject: [PATCH 1/9] Rebuild Vibeflow as an n8n safety gate
---
.agents/plugins/marketplace.json | 20 +
.github/DISCUSSION_TEMPLATE/ideas.yml | 20 -
.github/DISCUSSION_TEMPLATE/show-and-tell.yml | 15 -
.github/ISSUE_TEMPLATE/bug.yml | 22 +
.github/ISSUE_TEMPLATE/bug_report.md | 28 -
.github/ISSUE_TEMPLATE/config.yml | 8 +-
.github/ISSUE_TEMPLATE/false-positive.yml | 23 +
.github/ISSUE_TEMPLATE/feature_request.md | 20 -
.github/ISSUE_TEMPLATE/good-first-issue.md | 25 -
.github/ISSUE_TEMPLATE/rule-proposal.yml | 33 +
.github/labels-suggested.md | 17 -
.github/pull_request_template.md | 19 +-
.github/release-checklist.md | 10 -
.github/workflows/ci.yml | 61 ++
.github/workflows/markdown-lint.yml | 16 -
.gitignore | 7 +-
.vibeflow.json | 14 +
CHANGELOG.md | 94 +-
CODE_OF_CONDUCT.md | 7 +
CONTRIBUTING.md | 86 +-
LICENSE | 2 +-
README.md | 507 ++--------
SECURITY.md | 51 +-
VERSION | 1 -
action.yml | 34 +
assets/README.md | 22 -
assets/final-report-demo.svg | 13 -
assets/hero-banner.svg | 14 -
assets/logo.svg | 8 -
assets/social-preview.svg | 14 -
assets/terminal-demo.svg | 17 -
assets/workflow-demo.svg | 24 -
bin/vibeflow.mjs | 115 +++
clients/claude-code/README.md | 27 -
clients/claude-code/config-snippets.md | 36 -
clients/claude-code/sample-mcp.json | 12 -
clients/codex/README.md | 28 -
clients/codex/config-snippets.md | 31 -
clients/codex/sample-config.toml | 10 -
clients/openclaude/README.md | 11 -
clients/openclaude/config-snippets.md | 23 -
clients/opencode/README.md | 21 -
clients/opencode/config-snippets.md | 31 -
clients/opencode/sample-opencode.jsonc | 13 -
docs/architecture.md | 167 +---
docs/brand-kit.md | 37 -
docs/codex-for-oss-application.md | 53 ++
docs/community-onboarding.md | 32 -
docs/conversation-contract.md | 106 ---
docs/demo-assets.md | 48 -
docs/demo-script.md | 64 --
docs/demo.md | 32 +
docs/first-issues.md | 31 -
docs/getting-started.md | 72 --
docs/github-launch.md | 49 -
docs/install.md | 47 -
docs/launch-assets.md | 54 --
docs/launch-day-checklist.md | 38 -
docs/launch.md | 16 +
docs/product-brief.md | 47 +
docs/publishing-guide.md | 106 ---
docs/readme-premium-checklist.md | 38 -
docs/real-demo-playbook.md | 59 --
docs/recipes-gallery.md | 58 --
docs/release-audit.md | 54 ++
docs/release-notes-template.md | 36 -
docs/release-v0.7.0.md | 15 -
docs/roadmap.md | 39 +-
docs/showcase-checklist.md | 30 -
docs/skill-spec.md | 152 ---
docs/social-copy.md | 29 -
docs/tutorial-subir-github.md | 236 -----
examples/example-briefs.md | 10 -
examples/example-final-report.md | 30 -
examples/example-plans.md | 29 -
examples/example-walkthroughs.md | 39 -
examples/safe-support-agent.workflow.json | 99 ++
examples/sample-plan.json | 105 --
examples/sample-workflow-export.json | 57 --
examples/unsafe-support-agent.workflow.json | 43 +
package-lock.json | 19 +
package.json | 38 +
plugins/vibeflow/.codex-plugin/plugin.json | 28 +
plugins/vibeflow/skills/vibeflow/SKILL.md | 40 +
.../skills/vibeflow/agents/openai.yaml | 4 +
.../skills/vibeflow/references/policies.md | 16 +
recipes/ai-lead-enrichment.md | 48 -
recipes/customer-support-escalation.md | 35 -
recipes/hubspot-to-slack-qualification.md | 34 -
recipes/invoice-reminder.md | 17 -
recipes/lead-triage.md | 51 -
recipes/slack-to-notion-triage.md | 47 -
recipes/support-triage.md | 23 -
schemas/plan.schema.json | 171 ----
schemas/vibeflow-config.schema.json | 50 +
src/vibeflow.mjs | 900 ++++++++++++++++++
templates/final-report-template.md | 35 -
templates/intake-checklist.md | 38 -
templates/system-prompt.md | 131 ---
test/fixtures/invalid.workflow.json | 3 +
test/vibeflow.test.mjs | 467 +++++++++
101 files changed, 2405 insertions(+), 3557 deletions(-)
create mode 100644 .agents/plugins/marketplace.json
delete mode 100644 .github/DISCUSSION_TEMPLATE/ideas.yml
delete mode 100644 .github/DISCUSSION_TEMPLATE/show-and-tell.yml
create mode 100644 .github/ISSUE_TEMPLATE/bug.yml
delete mode 100644 .github/ISSUE_TEMPLATE/bug_report.md
create mode 100644 .github/ISSUE_TEMPLATE/false-positive.yml
delete mode 100644 .github/ISSUE_TEMPLATE/feature_request.md
delete mode 100644 .github/ISSUE_TEMPLATE/good-first-issue.md
create mode 100644 .github/ISSUE_TEMPLATE/rule-proposal.yml
delete mode 100644 .github/labels-suggested.md
delete mode 100644 .github/release-checklist.md
create mode 100644 .github/workflows/ci.yml
delete mode 100644 .github/workflows/markdown-lint.yml
create mode 100644 .vibeflow.json
create mode 100644 CODE_OF_CONDUCT.md
delete mode 100644 VERSION
create mode 100644 action.yml
delete mode 100644 assets/README.md
delete mode 100644 assets/final-report-demo.svg
delete mode 100644 assets/hero-banner.svg
delete mode 100644 assets/logo.svg
delete mode 100644 assets/social-preview.svg
delete mode 100644 assets/terminal-demo.svg
delete mode 100644 assets/workflow-demo.svg
create mode 100755 bin/vibeflow.mjs
delete mode 100644 clients/claude-code/README.md
delete mode 100644 clients/claude-code/config-snippets.md
delete mode 100644 clients/claude-code/sample-mcp.json
delete mode 100644 clients/codex/README.md
delete mode 100644 clients/codex/config-snippets.md
delete mode 100644 clients/codex/sample-config.toml
delete mode 100644 clients/openclaude/README.md
delete mode 100644 clients/openclaude/config-snippets.md
delete mode 100644 clients/opencode/README.md
delete mode 100644 clients/opencode/config-snippets.md
delete mode 100644 clients/opencode/sample-opencode.jsonc
delete mode 100644 docs/brand-kit.md
create mode 100644 docs/codex-for-oss-application.md
delete mode 100644 docs/community-onboarding.md
delete mode 100644 docs/conversation-contract.md
delete mode 100644 docs/demo-assets.md
delete mode 100644 docs/demo-script.md
create mode 100644 docs/demo.md
delete mode 100644 docs/first-issues.md
delete mode 100644 docs/getting-started.md
delete mode 100644 docs/github-launch.md
delete mode 100644 docs/install.md
delete mode 100644 docs/launch-assets.md
delete mode 100644 docs/launch-day-checklist.md
create mode 100644 docs/launch.md
create mode 100644 docs/product-brief.md
delete mode 100644 docs/publishing-guide.md
delete mode 100644 docs/readme-premium-checklist.md
delete mode 100644 docs/real-demo-playbook.md
delete mode 100644 docs/recipes-gallery.md
create mode 100644 docs/release-audit.md
delete mode 100644 docs/release-notes-template.md
delete mode 100644 docs/release-v0.7.0.md
delete mode 100644 docs/showcase-checklist.md
delete mode 100644 docs/skill-spec.md
delete mode 100644 docs/social-copy.md
delete mode 100644 docs/tutorial-subir-github.md
delete mode 100644 examples/example-briefs.md
delete mode 100644 examples/example-final-report.md
delete mode 100644 examples/example-plans.md
delete mode 100644 examples/example-walkthroughs.md
create mode 100644 examples/safe-support-agent.workflow.json
delete mode 100644 examples/sample-plan.json
delete mode 100644 examples/sample-workflow-export.json
create mode 100644 examples/unsafe-support-agent.workflow.json
create mode 100644 package-lock.json
create mode 100644 package.json
create mode 100644 plugins/vibeflow/.codex-plugin/plugin.json
create mode 100644 plugins/vibeflow/skills/vibeflow/SKILL.md
create mode 100644 plugins/vibeflow/skills/vibeflow/agents/openai.yaml
create mode 100644 plugins/vibeflow/skills/vibeflow/references/policies.md
delete mode 100644 recipes/ai-lead-enrichment.md
delete mode 100644 recipes/customer-support-escalation.md
delete mode 100644 recipes/hubspot-to-slack-qualification.md
delete mode 100644 recipes/invoice-reminder.md
delete mode 100644 recipes/lead-triage.md
delete mode 100644 recipes/slack-to-notion-triage.md
delete mode 100644 recipes/support-triage.md
delete mode 100644 schemas/plan.schema.json
create mode 100644 schemas/vibeflow-config.schema.json
create mode 100644 src/vibeflow.mjs
delete mode 100644 templates/final-report-template.md
delete mode 100644 templates/intake-checklist.md
delete mode 100644 templates/system-prompt.md
create mode 100644 test/fixtures/invalid.workflow.json
create mode 100644 test/vibeflow.test.mjs
diff --git a/.agents/plugins/marketplace.json b/.agents/plugins/marketplace.json
new file mode 100644
index 0000000..f372a0c
--- /dev/null
+++ b/.agents/plugins/marketplace.json
@@ -0,0 +1,20 @@
+{
+ "name": "vibeflow",
+ "interface": {
+ "displayName": "Vibeflow"
+ },
+ "plugins": [
+ {
+ "name": "vibeflow",
+ "source": {
+ "source": "local",
+ "path": "./plugins/vibeflow"
+ },
+ "policy": {
+ "installation": "AVAILABLE",
+ "authentication": "ON_INSTALL"
+ },
+ "category": "Developer Tools"
+ }
+ ]
+}
diff --git a/.github/DISCUSSION_TEMPLATE/ideas.yml b/.github/DISCUSSION_TEMPLATE/ideas.yml
deleted file mode 100644
index debe7dc..0000000
--- a/.github/DISCUSSION_TEMPLATE/ideas.yml
+++ /dev/null
@@ -1,20 +0,0 @@
-title: Ideas
-labels: [ideas]
-body:
- - type: textarea
- id: summary
- attributes:
- label: Idea summary
- description: What would you like to see in Vibeflow n8n?
- validations:
- required: true
- - type: textarea
- id: problem
- attributes:
- label: Problem
- description: What problem would this solve?
- - type: textarea
- id: sketch
- attributes:
- label: Proposed approach
- description: Share a rough implementation idea or usage example.
diff --git a/.github/DISCUSSION_TEMPLATE/show-and-tell.yml b/.github/DISCUSSION_TEMPLATE/show-and-tell.yml
deleted file mode 100644
index 500ad1d..0000000
--- a/.github/DISCUSSION_TEMPLATE/show-and-tell.yml
+++ /dev/null
@@ -1,15 +0,0 @@
-title: Show and tell
-labels: [show-and-tell]
-body:
- - type: textarea
- id: built
- attributes:
- label: What did you build?
- description: Share the workflow, recipe, or automation you created with Vibeflow n8n.
- validations:
- required: true
- - type: textarea
- id: notes
- attributes:
- label: Notes
- description: What worked well, what was tricky, and what should improve?
diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml
new file mode 100644
index 0000000..4b1c089
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug.yml
@@ -0,0 +1,22 @@
+name: Bug report
+description: Report incorrect CLI behavior
+title: "bug: "
+labels: [bug]
+body:
+ - type: textarea
+ attributes:
+ label: Minimal workflow
+ description: Attach an anonymized workflow or the smallest JSON that reproduces the problem. Never include credentials.
+ validations:
+ required: true
+ - type: textarea
+ attributes:
+ label: Command and output
+ render: shell
+ validations:
+ required: true
+ - type: input
+ attributes:
+ label: Vibeflow and Node.js versions
+ validations:
+ required: true
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
deleted file mode 100644
index 3b44a06..0000000
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ /dev/null
@@ -1,28 +0,0 @@
----
-name: Bug report
-about: Report a problem in behavior, docs, examples, or packaging
----
-
-## Summary
-
-Describe the bug.
-
-## Expected behavior
-
-What should have happened?
-
-## Actual behavior
-
-What happened instead?
-
-## Where it appears
-
-- [ ] docs
-- [ ] prompt / skill behavior
-- [ ] examples
-- [ ] client guide
-- [ ] recipes
-
-## Additional context
-
-Add logs, screenshots, or reproduction notes.
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
index 1d36c41..2c0f4e6 100644
--- a/.github/ISSUE_TEMPLATE/config.yml
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -1,5 +1,5 @@
-blank_issues_enabled: true
+blank_issues_enabled: false
contact_links:
- - name: Questions and setup help
- url: https://github.com/OWNER/REPO/discussions
- about: Use Discussions for setup questions, ideas, and help requests.
+ - name: Private security report
+ url: https://github.com/domfelipe/vibeflow-n8n/security/advisories/new
+ about: Report vulnerabilities and sensitive findings privately.
diff --git a/.github/ISSUE_TEMPLATE/false-positive.yml b/.github/ISSUE_TEMPLATE/false-positive.yml
new file mode 100644
index 0000000..cd2d98d
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/false-positive.yml
@@ -0,0 +1,23 @@
+name: False positive
+description: Report a policy finding that should not fire
+title: "false-positive: "
+labels: [false-positive]
+body:
+ - type: input
+ attributes:
+ label: Rule ID
+ placeholder: VF006
+ validations:
+ required: true
+ - type: textarea
+ attributes:
+ label: Anonymized workflow fragment
+ description: Include relevant nodes and connections without secrets or customer data.
+ render: json
+ validations:
+ required: true
+ - type: textarea
+ attributes:
+ label: Why the workflow is safe
+ validations:
+ required: true
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
deleted file mode 100644
index cfbfe29..0000000
--- a/.github/ISSUE_TEMPLATE/feature_request.md
+++ /dev/null
@@ -1,20 +0,0 @@
----
-name: Feature request
-about: Suggest an improvement, new recipe, or new client support idea
----
-
-## Summary
-
-Describe the improvement.
-
-## Why it matters
-
-What problem does it solve?
-
-## Proposed shape
-
-Describe your preferred implementation.
-
-## Additional context
-
-Links, examples, or related tools.
diff --git a/.github/ISSUE_TEMPLATE/good-first-issue.md b/.github/ISSUE_TEMPLATE/good-first-issue.md
deleted file mode 100644
index 4fb1564..0000000
--- a/.github/ISSUE_TEMPLATE/good-first-issue.md
+++ /dev/null
@@ -1,25 +0,0 @@
----
-name: Good first issue
-about: Template for starter tasks to help new contributors join the project.
-title: "[good first issue] "
-labels: ["good first issue"]
-assignees: []
----
-
-## Goal
-
-Describe the task in one or two sentences.
-
-## Why it matters
-
-Explain how this improves the project.
-
-## Acceptance criteria
-
-- [ ]
-- [ ]
-- [ ]
-
-## Helpful context
-
-Add links, files, or examples that make this easier to complete.
diff --git a/.github/ISSUE_TEMPLATE/rule-proposal.yml b/.github/ISSUE_TEMPLATE/rule-proposal.yml
new file mode 100644
index 0000000..b8b6efb
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/rule-proposal.yml
@@ -0,0 +1,33 @@
+name: Policy rule proposal
+description: Propose a deterministic check backed by exported workflow JSON
+title: "rule: "
+labels: [enhancement]
+body:
+ - type: textarea
+ id: failure
+ attributes:
+ label: Production failure
+ description: What concrete failure would this rule prevent?
+ validations:
+ required: true
+ - type: textarea
+ id: evidence
+ attributes:
+ label: Static evidence
+ description: Which exported JSON fields prove the unsafe and safe cases?
+ validations:
+ required: true
+ - type: textarea
+ id: fixtures
+ attributes:
+ label: Minimal fixtures
+ description: Paste anonymized unsafe and safe examples, or link to a branch containing them.
+ validations:
+ required: true
+ - type: textarea
+ id: false-positives
+ attributes:
+ label: False-positive boundary
+ description: When should this rule deliberately stay silent?
+ validations:
+ required: true
diff --git a/.github/labels-suggested.md b/.github/labels-suggested.md
deleted file mode 100644
index 5bef62a..0000000
--- a/.github/labels-suggested.md
+++ /dev/null
@@ -1,17 +0,0 @@
-# Suggested labels
-
-Create these labels after launch:
-
-- good first issue
-- help wanted
-- documentation
-- recipes
-- schemas
-- design
-- release
-- ideas
-- show-and-tell
-- client:codex
-- client:claude-code
-- client:opencode
-- client:openclaude
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
index 68ddb19..7fdea77 100644
--- a/.github/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -1,14 +1,11 @@
-## What changed
+## Risk addressed
-Describe the main changes in this PR.
+Describe the workflow failure or false positive this change addresses.
-## Why
+## Evidence
-Explain the problem or improvement.
-
-## Checklist
-
-- [ ] Docs updated if behavior changed
-- [ ] Examples updated if needed
-- [ ] Changelog updated if user-visible
-- [ ] No unsafe assumptions introduced
+- [ ] Added or updated a minimal unsafe fixture.
+- [ ] Added a safe fixture or regression test.
+- [ ] `npm run verify` passes.
+- [ ] No credentials or customer data are included.
+- [ ] Policy and Codex documentation match the implementation.
diff --git a/.github/release-checklist.md b/.github/release-checklist.md
deleted file mode 100644
index 8cdd9eb..0000000
--- a/.github/release-checklist.md
+++ /dev/null
@@ -1,10 +0,0 @@
-# Release checklist
-
-- bump `VERSION`
-- update `CHANGELOG.md`
-- verify README links
-- verify placeholder endpoints are clearly marked
-- test one workflow recipe end to end
-- create Git tag
-- publish GitHub release notes
-- announce supported clients and best-effort clients clearly
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..6bd3e04
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,61 @@
+name: CI
+
+on:
+ push:
+ branches: [main, "codex/**"]
+ pull_request:
+ branches: [main]
+
+permissions:
+ contents: read
+
+jobs:
+ test:
+ name: Node ${{ matrix.node-version }}
+ runs-on: ubuntu-latest
+ timeout-minutes: 10
+ strategy:
+ fail-fast: false
+ matrix:
+ node-version: [20, 22, 24]
+ steps:
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
+ with:
+ persist-credentials: false
+ - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
+ with:
+ node-version: ${{ matrix.node-version }}
+ cache: npm
+ - run: npm ci --ignore-scripts
+ - run: npm test
+ - run: npm run check
+ - run: npm pack --dry-run
+
+ integration:
+ name: GitHub Action and policy fixtures
+ runs-on: ubuntu-latest
+ timeout-minutes: 10
+ steps:
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
+ with:
+ persist-credentials: false
+ - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
+ with:
+ node-version: 22
+ - name: Exercise the local GitHub Action
+ uses: ./
+ with:
+ path: examples/safe-support-agent.workflow.json
+ output: vibeflow.sarif
+ - name: Ensure unsafe fixture is rejected
+ run: |
+ if node bin/vibeflow.mjs check examples/unsafe-support-agent.workflow.json --format sarif --output unsafe.sarif; then
+ echo "Unsafe fixture unexpectedly passed"
+ exit 1
+ fi
+ - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
+ with:
+ name: vibeflow-sarif
+ path: |
+ vibeflow.sarif
+ unsafe.sarif
diff --git a/.github/workflows/markdown-lint.yml b/.github/workflows/markdown-lint.yml
deleted file mode 100644
index ee57407..0000000
--- a/.github/workflows/markdown-lint.yml
+++ /dev/null
@@ -1,16 +0,0 @@
-name: Markdown Check
-
-on:
- push:
- pull_request:
-
-jobs:
- markdown-check:
- runs-on: ubuntu-latest
- steps:
- - name: Checkout
- uses: actions/checkout@v4
-
- - name: List markdown files
- run: |
- find . -type f \( -name "*.md" -o -name "VERSION" \) | sort
diff --git a/.gitignore b/.gitignore
index daf7693..a9ff8c5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,5 +9,8 @@
.vscode/
.idea/
-# demo exports
-assets/private/
+# local build output
+node_modules/
+*.tgz
+vibeflow.sarif
+.vibeflow.local.json
diff --git a/.vibeflow.json b/.vibeflow.json
new file mode 100644
index 0000000..93fd227
--- /dev/null
+++ b/.vibeflow.json
@@ -0,0 +1,14 @@
+{
+ "$schema": "./schemas/vibeflow-config.schema.json",
+ "rules": {
+ "VF001": "error",
+ "VF002": "error",
+ "VF003": "warning",
+ "VF004": "warning",
+ "VF005": "warning",
+ "VF006": "error",
+ "VF007": "warning",
+ "VF008": "warning",
+ "VF009": "warning"
+ }
+}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d3f5bc4..367307e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,91 +1,13 @@
# Changelog
-## 0.7.0
+## 0.8.0 - 2026-07-22
-- refreshed README for a stronger public-facing landing experience
-- added a real-demo playbook for terminal-to-n8n walkthroughs
-- added README premium checklist for launch polish
-- added customer support escalation and HubSpot-to-Slack qualification recipes
-- added an illustrative sample workflow export JSON for documentation and local experimentation
-- added release notes for v0.7.0
+- Repositioned Vibeflow as a safety and contract gate for AI-generated n8n workflows.
+- Added a dependency-free CLI with text, JSON, and SARIF reports.
+- Added VF000-VF009 policies, configuration, fixtures, tests, and a GitHub Action.
+- Added an installable Codex skill and plugin marketplace.
+- Removed the obsolete workflow-builder marketing kit.
-## 0.6.0
+## Legacy
-- added showcase-oriented demo docs
-- added placeholder SVG assets for hero, terminal, workflow, and report visuals
-- added AI lead enrichment and Slack-to-Notion triage recipes
-- updated README to highlight demo flow and stronger launch materials
-- updated roadmap for a showcase-ready release
-
-## 0.5.0
-
-- Added launch-ready branding files, including logo and social preview SVG assets
-- Added community onboarding docs, starter issue ideas, and social copy
-- Added GitHub publishing tutorial with both web and CLI flows
-- Added Discussion templates and an extra good-first-issue template
-- Refined README for public launch positioning
-
-## v0.4.0
-
-Launch-focused release that makes the repository ready for public publication on GitHub.
-
-### Added
-- launch assets and copy pack for GitHub, socials, and repository metadata
-- reusable release notes template
-- sample MCP config files for Codex, Claude Code, and OpenCode
-- starter screenshots guide and demo capture checklist
-- launch-day checklist with publishing order and post-launch follow-up
-- README polish for public-facing adoption
-
-### Improved
-- clearer positioning for the project as a planning-first skill kit
-- stronger onboarding guidance for first-time users
-- better launch readiness for open-source publication
-
-## v0.3.0
-
-GitHub-ready skill kit with schema, install guides, and client snippets.
-
-### Added
-- getting started guide
-- install/setup docs
-- normalized plan schema
-- sample plan JSON
-- release checklist
-- concrete client snippets for Codex, Claude Code, OpenCode
-- OpenClaude compatibility notes
-
-### Improved
-- README structure and public-facing docs
-- packaging for publication
-
-## v0.2.0
-
-Second public iteration focused on open-source readiness and documentation maturity.
-
-### Added
-- bilingual README
-- architecture and roadmap docs
-- contributing, security, and changelog files
-- recipes for lead triage, support triage, and invoice reminders
-- walkthrough examples
-- GitHub issue and PR templates
-- starter markdown lint workflow
-
-### Improved
-- project structure for public publishing
-- skill prompt and conversation contract
-
-## v0.1.0
-
-Initial public skeleton of the Vibeflow n8n skill kit.
-
-### Added
-- repository structure
-- initial README
-- skill spec
-- conversation contract
-- publishing guide
-- system prompt
-- intake and final report templates
-- example plans and briefs
+The original documentation-first prototype is preserved at `legacy-v0.7.0`.
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000..ffabd84
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,7 @@
+# Code of conduct
+
+Vibeflow welcomes evidence-backed contributions from people of every background and experience level.
+
+Be respectful, discuss the work rather than the person, protect private workflow data, and assume good intent while asking for reproducible evidence. Harassment, discrimination, threats, doxxing, and publication of credentials or customer data are not acceptable.
+
+Report conduct or privacy concerns privately to the maintainer through the contact address on the maintainer's GitHub profile. Reports will be reviewed confidentially and may result in warnings, content removal, or exclusion from project spaces.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index ce8affc..661d567 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,79 +1,29 @@
# Contributing
-Thanks for contributing to Vibeflow n8n.
+Vibeflow favors small, evidence-backed rules over broad heuristics.
-We want this project to stay practical, readable, and friendly to contributors.
+## Development
-## What contributions are welcome
+Node.js 20 or newer is required. No dependency installation is needed.
-- new recipes
-- better examples
-- client-specific setup improvements
-- docs clarifications
-- prompt refinements
-- validation heuristics
-- packaging improvements for open-source distribution
+```bash
+npm test
+npm run check
+npm pack --dry-run
+```
-## Contribution principles
+## Policy changes
-- prefer clarity over cleverness
-- keep examples realistic
-- avoid vendor lock where possible
-- document assumptions explicitly
-- keep the user experience friendly for non-experts
+A policy change must include:
-## Branch naming
+1. A stable rule ID and remediation in `src/vibeflow.mjs`.
+2. Configuration support in the JSON schema.
+3. A fixture proving the unsafe case.
+4. A safe fixture or test guarding against the likely false positive.
+5. Updated user and Codex policy documentation.
-Suggested branch prefixes:
-- `feat/`
-- `fix/`
-- `docs/`
-- `chore/`
-- `recipe/`
+Rules that cannot produce deterministic results from exported JSON belong outside the core CLI.
-Examples:
-- `feat/add-plan-schema`
-- `docs/improve-opencode-guide`
-- `recipe/lead-qualification-pack`
+Keep pull requests focused. Show the smallest failing workflow and the before/after CLI output. Never submit real credentials or customer data.
-## Pull requests
-
-A good PR should:
-- explain the problem,
-- describe the change,
-- note any breaking behavior,
-- include updated docs when needed,
-- include an example when behavior changes.
-
-## Documentation expectations
-
-If you change the skill behavior, also update at least one of:
-- `docs/skill-spec.md`
-- `docs/conversation-contract.md`
-- `templates/system-prompt.md`
-- `examples/`
-
-## Recipes
-
-When adding a recipe, include:
-- scenario summary,
-- ideal intake questions,
-- suggested node structure,
-- common risks,
-- validation notes,
-- sample final report excerpt.
-
-## Style guide
-
-- use markdown
-- keep sections short and scannable
-- write for builders, not only prompt engineers
-- avoid unnecessary jargon
-
-## Release notes
-
-When your change matters to users, update `CHANGELOG.md`.
-
-## Security-sensitive changes
-
-If your contribution touches secrets, auth, external communication, or destructive actions, also review `SECURITY.md`.
+Participation is governed by the [code of conduct](CODE_OF_CONDUCT.md).
diff --git a/LICENSE b/LICENSE
index 14fac91..824104e 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
MIT License
-Copyright (c) 2026
+Copyright (c) 2026 Felipe Domingues
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/README.md b/README.md
index 738d778..d2af04a 100644
--- a/README.md
+++ b/README.md
@@ -1,471 +1,112 @@
- # β‘ Vibeflow n8n
+# Vibeflow
-
-
+[](https://github.com/domfelipe/vibeflow-n8n/actions/workflows/ci.yml)
+[](LICENSE)
-### Build complete n8n workflows through MCP with any coding agent.
+**Safety and contract checks for AI-generated n8n workflows.**
-**Vibe code your automation. Let the agent handle the wiring.**
+Vibeflow answers one question before deployment: **does this workflow deserve to reach production?**
-Create, update, and validate n8n workflows using **Codex CLI**, **Claude Code**, **OpenCode**, and other MCP-capable coding agents.
+It inspects exported n8n JSON for embedded secrets, dangerous nodes, exposed webhooks, missing failure paths, absent idempotency, unsafe AI paths, unbounded execution, and risky retries.
-[](#-release-notes)
-[](#-why-vibeflow)
-[](#-supported-clients)
-[](./LICENSE)
-[](#-contributing)
+Vibeflow is not another workflow builder or MCP server. It is a deterministic quality gate for workflows built by people or agents.
-
-
----
-
-## β¨ What is Vibeflow n8n?
-
-**Vibeflow n8n** is an open-source skill kit for coding agents that lets anyone describe an automation in natural language and have the agent:
-
-1. **ask the right questions**
-2. **generate a structured plan**
-3. **build the workflow in n8n through MCP**
-4. **validate the result**
-5. **return a clean implementation report**
-
-Instead of wrestling with nodes, expressions, branching logic, and half-finished drafts, you get a guided workflow-building experience designed for **vibe coding with real structure**.
-
----
-
-## π₯ Why Vibeflow?
-
-Most agent-based automation flows fail for predictable reasons:
-
-* they start building too early
-* they skip business rules
-* they assume credentials and app behavior
-* they produce fragile workflows with no validation
-* they leave the user with a maze instead of an automation
-
-**Vibeflow n8n** fixes that by forcing a better sequence:
-
-* **brief first** π§
-* **plan next** πΊοΈ
-* **build with MCP** π§
-* **validate before delivery** β
-* **report what was done and what is missing** π¦
-
-It is opinionated in the useful places and flexible where real-world workflows vary.
-
----
-
-## π§ How it works
-
-```mermaid
-flowchart LR
- A[User describes workflow] --> B[Agent runs intake]
- B --> C[Plan is normalized]
- C --> D[Workflow built via n8n MCP]
- D --> E[Validation and assumptions]
- E --> F[Final delivery report]
-```
-
-### The default flow
-
-* **Intake**: understand goal, trigger, systems, outputs, business rules, and exceptions
-* **Planning**: transform free-form input into a structured build plan
-* **Execution**: create or update the workflow in n8n through MCP
-* **Validation**: inspect missing credentials, placeholders, trigger logic, branches, and node naming
-* **Delivery**: summarize what was created and what still needs manual configuration
-
----
-
-## π§© Supported clients
-
-Vibeflow is built for **MCP-capable coding agents**.
-
-### Officially documented in this repo
-
-* **Codex CLI**
-* **Claude Code**
-* **OpenCode**
-* **Community / experimental MCP clients**
-
-See:
-
-* [`clients/codex/`](./clients/codex)
-* [`clients/claude-code/`](./clients/claude-code)
-* [`clients/opencode/`](./clients/opencode)
-* [`clients/openclaude/`](./clients/openclaude)
-
----
-
-## π οΈ What you can build
-
-### Growth and ops
-
-* lead triage pipelines
-* AI lead enrichment
-* CRM qualification flows
-* inbound webhook routers
-* campaign handoff automations
-
-### Support workflows
-
-* support triage
-* escalation routing
-* Slack to Notion triage
-* ticket classification and enrichment
-
-### Finance and back office
-
-* invoice reminders
-* approval flows
-* billing follow-ups
-* notification pipelines
-
-### AI-powered workflows
-
-* summarize incoming data
-* classify requests
-* enrich records with LLM outputs
-* hand off low-confidence cases for review
-
----
-
-## π Quick start
-
-### 1. Clone the repository
+## Quick start
```bash
-git clone https://github.com/domfelipe/vibeflow-n8n.git
-cd vibeflow-n8n
+npx --yes github:domfelipe/vibeflow-n8n#v0.8.0 check workflow.json
```
-### 2. Connect an MCP-compatible coding agent
+Or from a checkout:
-Pick your client and configure it using the examples in the `clients/` folder.
+```bash
+node bin/vibeflow.mjs check workflow.json
+```
-### 3. Point your agent to Vibeflow instructions
-
-Use the prompt and behavioral contract from:
-
-* [`templates/system-prompt.md`](./templates/system-prompt.md)
-* [`docs/conversation-contract.md`](./docs/conversation-contract.md)
-* [`docs/skill-spec.md`](./docs/skill-spec.md)
-
-### 4. Ask for a workflow
-
-Example prompt:
+A safe workflow exits `0`. Blocking findings exit `1`; invalid usage exits `2`.
```text
-Build an n8n workflow that receives leads from a webhook, enriches them with AI, sends qualified leads to HubSpot, and notifies Slack when confidence is low.
+β examples/unsafe-support-agent.workflow.json (Unsafe support agent)
+ ERROR VF001 [Send response] Literal secret-like value
+ ERROR VF002 [Run Shell] Host-level node is blocked
+ ERROR VF006 [AI Agent] No upstream kill switch
+
+Checked 1 workflow(s): 3 error(s), 8 warning(s)
```
-### 5. Review the plan, then build
+Run the reproducible fixtures:
-The agent should:
+```bash
+node bin/vibeflow.mjs check examples/safe-support-agent.workflow.json
+node bin/vibeflow.mjs check examples/unsafe-support-agent.workflow.json --fail-on never
+```
-* ask follow-up questions only when needed
-* create a structured plan
-* build in n8n via MCP
-* validate the result
-* deliver a final report
+## Policies
----
+| ID | Default | Check |
+|---|---|---|
+| VF000 | error | Valid n8n workflow JSON |
+| VF001 | error | Literal credentials in node parameters |
+| VF002 | error | Host-level command, SSH, and local-file nodes |
+| VF003 | warning | Webhooks without supported auth and a credential reference |
+| VF004 | warning | External actions without a connected failure path |
+| VF005 | warning | Inbound side effects without an atomic idempotency claim |
+| VF006 | error | AI entry paths that bypass a structural kill switch |
+| VF007 | warning | AI paths without a reachable external human handoff |
+| VF008 | warning | Workflows without a 1-3600 second execution timeout |
+| VF009 | warning | Retries without idempotency, bounds, or backoff |
-## π§ Core design principles
+Static analysis cannot prove runtime correctness. Configure severity and domain vocabulary in `.vibeflow.json`; document every waiver.
-### 1. Plan before touching n8n
+Use `--locked` in untrusted CI. It rejects disabled or downgraded rules, changed vocabulary, and removal of default banned node types. The bundled GitHub Action always enables it.
-No blind node generation.
+## Automation
-### 2. Ask only what changes the build
+```bash
+vibeflow check workflow.json --format json
+vibeflow check workflow.json --format sarif --output vibeflow.sarif
+vibeflow check workflows/ --fail-on warning
+```
-No interrogation theater.
+Directories are searched recursively for `*.workflow.json` files.
-### 3. Defaults should be helpful
+### GitHub Action
-The agent should use safe, practical assumptions when possible.
+```yaml
+- uses: actions/checkout@v4
+- uses: domfelipe/vibeflow-n8n@v0.8.0
+ with:
+ path: workflows/
+ output: vibeflow.sarif
+```
-### 4. Report assumptions clearly
+## Codex plugin
-What was inferred should never be hidden.
+```bash
+codex plugin marketplace add domfelipe/vibeflow-n8n
+```
-### 5. Output should be editable by humans
-
-The workflow must still make sense inside n8n.
-
----
-
-## π Repository structure
+Install **Vibeflow** from the Plugins Directory, then ask:
```text
-vibeflow-n8n/
-βββ clients/
-β βββ claude-code/
-β βββ codex/
-β βββ opencode/
-β βββ openclaude/
-βββ docs/
-β βββ architecture.md
-β βββ conversation-contract.md
-β βββ getting-started.md
-β βββ github-launch.md
-β βββ install.md
-β βββ roadmap.md
-β βββ tutorial-subir-github.md
-βββ examples/
-β βββ sample-plan.json
-β βββ sample-workflow-export.json
-βββ recipes/
-β βββ ai-lead-enrichment.md
-β βββ customer-support-escalation.md
-β βββ hubspot-to-slack-qualification.md
-β βββ invoice-reminder.md
-β βββ lead-triage.md
-β βββ slack-to-notion-triage.md
-β βββ support-triage.md
-βββ schemas/
-β βββ plan.schema.json
-βββ templates/
-β βββ final-report-template.md
-β βββ intake-checklist.md
-β βββ system-prompt.md
-βββ README.md
+Use $vibeflow to audit this n8n workflow and fix blocking findings.
```
----
+## Boundaries
-## βοΈ Build contract
+No hosted service, new MCP server, workflow generation, telemetry, secret collection, or live n8n mutation. The CLI uses only the Node.js 20+ standard library.
-Vibeflow expects agents to move through these stages:
+## Documentation
-### Intake
+- [Product brief](docs/product-brief.md)
+- [Architecture and limitations](docs/architecture.md)
+- [Reproducible demo](docs/demo.md)
+- [v0.8.0 release audit](docs/release-audit.md)
+- [Roadmap](docs/roadmap.md)
+- [Codex for Open Source application gate](docs/codex-for-oss-application.md)
+- [Contributing](CONTRIBUTING.md)
+- [Security](SECURITY.md)
-Capture:
+`v0.8.0` is the first executable release. The original documentation prototype is preserved at `legacy-v0.7.0`.
-* workflow goal
-* trigger type
-* systems involved
-* desired output
-* business rules
-* exceptions
-* approval needs
-
-### Planning
-
-Generate a normalized plan with:
-
-* summary
-* trigger
-* apps and services
-* implementation steps
-* edge cases
-* validation checklist
-* required credentials
-* unresolved questions
-
-### Execution
-
-Use MCP to:
-
-* create or update the workflow
-* name nodes clearly
-* preserve readability
-* add placeholders where secrets are missing
-
-### Validation
-
-Check for:
-
-* broken branches
-* missing credentials
-* malformed assumptions
-* unclear node naming
-* weak error handling
-
-### Delivery
-
-Return:
-
-* what was built
-* what was assumed
-* what still needs manual setup
-* how to test it
-* suggested upgrades
-
----
-
-## π¦ Example use cases
-
-### Example 1: Lead routing
-
-> βBuild a workflow that receives website leads, scores them, enriches them with AI, and sends only qualified ones to HubSpot.β
-
-### Example 2: Support escalation
-
-> βCreate a workflow that watches urgent support requests, classifies them, escalates high-risk cases to Slack, and creates a tracking record.β
-
-### Example 3: Billing reminder
-
-> βBuild a recurring workflow that checks unpaid invoices every weekday and sends reminders only when due dates are inside policy.β
-
-See the full set in [`recipes/`](./recipes).
-
----
-
-## πΌοΈ Demo flow
-
-A clean public demo usually looks like this:
-
-1. show the natural-language request
-2. show the planning output
-3. show the workflow created in n8n
-4. show the final delivery report
-
-Helpful references:
-
-* [`docs/demo-script.md`](./docs/demo-script.md)
-* [`docs/real-demo-playbook.md`](./docs/real-demo-playbook.md)
-* [`docs/showcase-checklist.md`](./docs/showcase-checklist.md)
-
----
-
-## π Documentation
-
-### Start here
-
-* [`docs/getting-started.md`](./docs/getting-started.md)
-* [`docs/install.md`](./docs/install.md)
-* [`docs/architecture.md`](./docs/architecture.md)
-
-### Agent behavior
-
-* [`docs/skill-spec.md`](./docs/skill-spec.md)
-* [`docs/conversation-contract.md`](./docs/conversation-contract.md)
-* [`templates/system-prompt.md`](./templates/system-prompt.md)
-
-### Publishing and community
-
-* [`docs/github-launch.md`](./docs/github-launch.md)
-* [`docs/community-onboarding.md`](./docs/community-onboarding.md)
-* [`CONTRIBUTING.md`](./CONTRIBUTING.md)
-* [`SECURITY.md`](./SECURITY.md)
-
----
-
-## π Who is this for?
-
-Vibeflow is a good fit for:
-
-* automation builders using n8n
-* developers who prefer CLI agents
-* consultants delivering automation fast
-* internal ops teams
-* founders building workflows without a huge engineering ceremony
-* anyone who likes **vibe coding**, but also likes finishing things
-
----
-
-## π§ͺ Status
-
-Vibeflow is currently a **skill kit / workflow-building framework for coding agents**, with practical support material for MCP-based clients and progressive improvements across releases.
-
-If you want the fastest route to value, start with:
-
-* one recipe
-* one client
-* one end-to-end demo
-
-Then expand.
-
----
-
-## πΊοΈ Roadmap
-
-Planned areas of evolution:
-
-* richer client-specific setup examples
-* more production-grade workflow recipes
-* exportable demo workflows
-* stronger plan validation
-* public demo assets and GIFs
-* starter kits by domain
-
-See [`docs/roadmap.md`](./docs/roadmap.md).
-
----
-
-## π€ Contributing
-
-Contributions are welcome.
-
-You can help by:
-
-* improving docs
-* adding recipes
-* refining client setup guides
-* validating agent behavior in real workflows
-* contributing examples and demos
-
-Start here:
-
-* [`CONTRIBUTING.md`](./CONTRIBUTING.md)
-* [`.github/ISSUE_TEMPLATE/`](./.github/ISSUE_TEMPLATE)
-* [`.github/DISCUSSION_TEMPLATE/`](./.github/DISCUSSION_TEMPLATE)
-
----
-
-## π‘οΈ Security
-
-Please do **not** commit secrets, credentials, or private tokens.
-
-If you find a security issue, check [`SECURITY.md`](./SECURITY.md).
-
----
-
-## π Release notes
-
-Recent release materials:
-
-* [`docs/release-v0.7.0.md`](./docs/release-v0.7.0.md)
-* [`CHANGELOG.md`](./CHANGELOG.md)
-
----
-
-## π¬ Community
-
-This project is designed to become easier the more people share their patterns.
-
-If you build something cool with Vibeflow:
-
-* open a discussion
-* share your workflow idea
-* submit a recipe
-* improve the docs for the next builder
-
----
-
-## β Support the project
-
-If this repo helps you build better automations:
-
-* give it a star
-* share it with your team
-* open an issue with ideas
-* contribute a recipe or improvement
-
-That kind of signal helps the project grow legs.
-
----
-
-## π License
-
-This project is available under the terms of the license in [`LICENSE`](./LICENSE).
-
----
-
-
-
-### β‘ Vibeflow n8n
-
-**Describe the workflow. Vibe code the idea. Let the agent build the machinery.**
-
-
+MIT licensed.
diff --git a/SECURITY.md b/SECURITY.md
index 0e1f300..df4f78c 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -1,44 +1,15 @@
-# Security Policy
+# Security policy
+
+## Reporting
+
+Use GitHub private vulnerability reporting for this repository. Do not open a public issue containing exploit details, credentials, or customer workflow data.
+
+## Supported versions
+
+Security fixes target the latest tagged release.
## Scope
-This repository contains prompts, conventions, examples, and packaging guidance for agent-driven workflow creation in n8n.
+Vibeflow reads local JSON and optionally writes a caller-selected report. It does not contact n8n, execute workflows, collect telemetry, or upload workflow content.
-It does not store production secrets by design.
-
-## Reporting a vulnerability
-
-If you discover a security issue related to:
-- secret handling guidance,
-- destructive workflow defaults,
-- unsafe recipe recommendations,
-- risky prompt behavior,
-
-please report it privately before opening a public issue.
-
-Use a private contact method for the maintainer when available.
-
-## Security expectations for contributors
-
-Contributors should avoid introducing guidance that:
-- assumes access to credentials that may not exist,
-- performs destructive actions without explicit user intent,
-- sends external communications without confirmation,
-- hides compliance-sensitive assumptions,
-- suggests storing plaintext secrets in repo files.
-
-## Safe defaults
-
-The skill should prefer:
-- placeholders over fake credentials,
-- explicit assumptions over silent guesses,
-- confirmation for destructive or externally visible actions,
-- human-readable reports for manual review.
-
-## Out of scope
-
-This repository does not guarantee:
-- security of any third-party MCP server,
-- security of any n8n deployment,
-- security of a user's local machine,
-- correctness of external vendor SDKs or CLIs.
+The secret rule is a defensive heuristic, not a replacement for repository secret scanning. A clean report is not proof of runtime security.
diff --git a/VERSION b/VERSION
deleted file mode 100644
index faef31a..0000000
--- a/VERSION
+++ /dev/null
@@ -1 +0,0 @@
-0.7.0
diff --git a/action.yml b/action.yml
new file mode 100644
index 0000000..487a46c
--- /dev/null
+++ b/action.yml
@@ -0,0 +1,34 @@
+name: "Vibeflow n8n safety gate"
+description: "Audit an exported n8n workflow and fail on blocking safety findings"
+author: "Felipe Domingues"
+inputs:
+ path:
+ description: "Workflow JSON file or directory containing *.workflow.json files"
+ required: false
+ default: "."
+ format:
+ description: "Report format: text, json, or sarif"
+ required: false
+ default: "sarif"
+ output:
+ description: "Report output path"
+ required: false
+ default: "vibeflow.sarif"
+ fail-on:
+ description: "Failure threshold: error, warning, or never"
+ required: false
+ default: "error"
+runs:
+ using: "composite"
+ steps:
+ - name: Run Vibeflow
+ shell: bash
+ env:
+ VIBEFLOW_INPUT_PATH: ${{ inputs.path }}
+ VIBEFLOW_INPUT_FORMAT: ${{ inputs.format }}
+ VIBEFLOW_INPUT_OUTPUT: ${{ inputs.output }}
+ VIBEFLOW_INPUT_FAIL_ON: ${{ inputs.fail-on }}
+ run: >-
+ node "$GITHUB_ACTION_PATH/bin/vibeflow.mjs" check "$VIBEFLOW_INPUT_PATH"
+ --format "$VIBEFLOW_INPUT_FORMAT" --output "$VIBEFLOW_INPUT_OUTPUT"
+ --fail-on "$VIBEFLOW_INPUT_FAIL_ON" --locked
diff --git a/assets/README.md b/assets/README.md
deleted file mode 100644
index fc8989a..0000000
--- a/assets/README.md
+++ /dev/null
@@ -1,22 +0,0 @@
-# Assets
-
-This folder contains launch and showcase assets for the repository.
-
-## Included
-
-- `logo.svg`
-- `social-preview.svg`
-- `hero-banner.svg`
-- `terminal-demo.svg`
-- `workflow-demo.svg`
-- `final-report-demo.svg`
-
-## Usage
-
-Use these SVG files as placeholders until you capture real screenshots or GIFs from an end-to-end run.
-
-Recommended replacement order:
-1. terminal demo
-2. workflow canvas
-3. final report
-4. hero banner
diff --git a/assets/final-report-demo.svg b/assets/final-report-demo.svg
deleted file mode 100644
index bfe75c4..0000000
--- a/assets/final-report-demo.svg
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
-Implementation Report
-Created
-β’ Slack trigger, urgency classifier, Notion insert, status reporter
-Assumptions
-β’ Urgent means billing, outage, or executive mention
-Missing credentials
-β’ Slack bot token, Notion integration, escalation channel id
-How to test
-β’ Send one urgent message and one normal message, then confirm both paths
-
\ No newline at end of file
diff --git a/assets/hero-banner.svg b/assets/hero-banner.svg
deleted file mode 100644
index c2b284f..0000000
--- a/assets/hero-banner.svg
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-Vibeflow n8n
-Plan-first workflow generation for MCP-capable coding agents
-
-User:
-Build a Slack-to-Notion triage workflow with urgency routing.
-Agent:
-1) Asking only critical questions
-2) Generating normalized plan
-3) Building workflow in n8n via MCP
-Use this as a placeholder hero until you replace it with a real screenshot or GIF.
-
\ No newline at end of file
diff --git a/assets/logo.svg b/assets/logo.svg
deleted file mode 100644
index e833604..0000000
--- a/assets/logo.svg
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
-
-
- Vibeflow
-
diff --git a/assets/social-preview.svg b/assets/social-preview.svg
deleted file mode 100644
index fac3506..0000000
--- a/assets/social-preview.svg
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
- Vibeflow n8n
- Build complete n8n workflows through MCP with any coding agent.
- Plan β Build β Validate β Handoff
- Codex CLI Β· Claude Code Β· OpenCode Β· MCP-capable forks
-
- n8n via MCP
-
- Skill-first
-
- Open source
-
diff --git a/assets/terminal-demo.svg b/assets/terminal-demo.svg
deleted file mode 100644
index 50a8537..0000000
--- a/assets/terminal-demo.svg
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
-
-
-
-
-$ use vibeflow skill to build a support triage workflow
-Agent> What should trigger the workflow?
-User> New support emails tagged urgent or billing
-Agent> Which systems should receive the result?
-User> Zendesk, Slack, and a spreadsheet
-Agent> Generating normalized plan...
-β objective
-β trigger
-β branching logic
-β validation plan
-
\ No newline at end of file
diff --git a/assets/workflow-demo.svg b/assets/workflow-demo.svg
deleted file mode 100644
index b2d405c..0000000
--- a/assets/workflow-demo.svg
+++ /dev/null
@@ -1,24 +0,0 @@
-
-
-Sample Workflow Layout
-
-Slack Trigger
-
-Normalize Message
-
-Classify Urgency
-
-Urgent Path
-
-Normal Path
-
-Create Notion Item
-
-Report Status
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/bin/vibeflow.mjs b/bin/vibeflow.mjs
new file mode 100755
index 0000000..e2e0cb0
--- /dev/null
+++ b/bin/vibeflow.mjs
@@ -0,0 +1,115 @@
+#!/usr/bin/env node
+
+import { writeFile } from "node:fs/promises";
+import {
+ VERSION,
+ checkPaths,
+ formatJson,
+ formatSarif,
+ formatText,
+ loadConfig,
+ safeDisplay,
+} from "../src/vibeflow.mjs";
+
+const HELP = `Vibeflow ${VERSION}
+
+Usage:
+ vibeflow check ... [options]
+
+Options:
+ --config Policy configuration (default: .vibeflow.json)
+ --format text, json, or sarif (default: text)
+ --output Write the report to a file
+ --fail-on error, warning, or never (default: error)
+ --locked Reject configuration that weakens built-in policy
+ --version Print the version
+ --help Print this help
+
+Directories are searched recursively for *.workflow.json files.
+Exit codes: 0 passed, 1 policy failure, 2 invalid input or usage.`;
+
+function parseArgs(argv) {
+ const options = {
+ command: null,
+ inputs: [],
+ configPath: null,
+ format: "text",
+ output: null,
+ failOn: "error",
+ locked: false,
+ help: false,
+ version: false,
+ };
+
+ const args = [...argv];
+ while (args.length) {
+ const arg = args.shift();
+ if (arg === "--help" || arg === "-h") options.help = true;
+ else if (arg === "--version" || arg === "-v") options.version = true;
+ else if (arg === "--config") options.configPath = requireValue(arg, args);
+ else if (arg === "--format") options.format = requireValue(arg, args);
+ else if (arg === "--output") options.output = requireValue(arg, args);
+ else if (arg === "--fail-on") options.failOn = requireValue(arg, args);
+ else if (arg === "--locked") options.locked = true;
+ else if (arg.startsWith("-")) throw new Error(`Unknown option: ${arg}`);
+ else if (!options.command) options.command = arg;
+ else options.inputs.push(arg);
+ }
+
+ if (!options.help && !options.version) {
+ if (options.command !== "check") throw new Error("The only command is: check");
+ if (!options.inputs.length) throw new Error("Pass at least one workflow file or directory");
+ if (!["text", "json", "sarif"].includes(options.format)) {
+ throw new Error("--format must be text, json, or sarif");
+ }
+ if (!["error", "warning", "never"].includes(options.failOn)) {
+ throw new Error("--fail-on must be error, warning, or never");
+ }
+ }
+
+ return options;
+}
+
+function requireValue(option, args) {
+ const value = args.shift();
+ if (!value || value.startsWith("-")) throw new Error(`${option} requires a value`);
+ return value;
+}
+
+function shouldFail(report, failOn) {
+ if (failOn === "never") return false;
+ if (failOn === "warning") return report.summary.errors + report.summary.warnings > 0;
+ return report.summary.errors > 0;
+}
+
+async function main() {
+ try {
+ const options = parseArgs(process.argv.slice(2));
+ if (options.help) {
+ console.log(HELP);
+ return;
+ }
+ if (options.version) {
+ console.log(VERSION);
+ return;
+ }
+
+ const config = await loadConfig(options.configPath, process.cwd(), options.locked);
+ const report = await checkPaths(options.inputs, config);
+ const rendered = options.format === "json"
+ ? formatJson(report)
+ : options.format === "sarif"
+ ? formatSarif(report)
+ : formatText(report);
+
+ if (options.output) await writeFile(options.output, `${rendered}\n`, "utf8");
+ else console.log(rendered);
+
+ if (shouldFail(report, options.failOn)) process.exitCode = 1;
+ } catch (error) {
+ console.error(`vibeflow: ${safeDisplay(error.message)}`);
+ process.exitCode = 2;
+ }
+}
+
+await main();
diff --git a/clients/claude-code/README.md b/clients/claude-code/README.md
deleted file mode 100644
index d85ed44..0000000
--- a/clients/claude-code/README.md
+++ /dev/null
@@ -1,27 +0,0 @@
-# Claude Code
-
-## Goal
-
-Use Vibeflow n8n as a reusable instruction layer for building n8n workflows through MCP.
-
-## Recommended setup idea
-
-- connect Claude Code to the n8n MCP server
-- add the prompt from `templates/system-prompt.md` to your skill or instructions layer
-- keep the docs folder available so the agent can reference the behavior contract
-
-## Suggested operating mode
-
-The best default is `balanced` mode:
-- few but useful questions
-- planning before build
-- explicit assumptions
-- readable delivery report
-
-## Prompt seed
-
-```text
-Follow the Vibeflow n8n skill from this repository. Gather the minimum viable requirements, produce a normalized plan, create or update the workflow through MCP, validate it, and give me a concise final report.
-```
-
-See `config-snippets.md` for example `claude mcp add --scope project` and `.mcp.json` setup.
diff --git a/clients/claude-code/config-snippets.md b/clients/claude-code/config-snippets.md
deleted file mode 100644
index a728fd2..0000000
--- a/clients/claude-code/config-snippets.md
+++ /dev/null
@@ -1,36 +0,0 @@
-# Claude Code config snippets
-
-These snippets are examples. Replace placeholder URLs and names with your real n8n MCP configuration.
-
-## Add a project-scoped HTTP MCP server
-
-```bash
-claude mcp add --transport http --scope project n8n https://YOUR-N8N-MCP-ENDPOINT
-```
-
-This writes a `.mcp.json` file at the project root.
-
-## Example `.mcp.json`
-
-```json
-{
- "mcpServers": {
- "n8n": {
- "type": "http",
- "url": "https://YOUR-N8N-MCP-ENDPOINT"
- }
- }
-}
-```
-
-## Optional instruction layer
-
-```text
-Use the Vibeflow n8n behavior in this repository. Ask only essential workflow questions, generate a normalized plan before any n8n changes, build through the n8n MCP server, and return a concise final report.
-```
-
-## Notes
-
-- `local` scope is private to your project entry inside `~/.claude.json`.
-- `project` scope creates a versionable `.mcp.json`.
-- `user` scope makes the server available across projects.
diff --git a/clients/claude-code/sample-mcp.json b/clients/claude-code/sample-mcp.json
deleted file mode 100644
index 04e0452..0000000
--- a/clients/claude-code/sample-mcp.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "mcpServers": {
- "n8n": {
- "command": "npx",
- "args": ["-y", "n8n-mcp"],
- "env": {
- "N8N_BASE_URL": "http://localhost:5678",
- "N8N_API_KEY": "replace-me"
- }
- }
- }
-}
diff --git a/clients/codex/README.md b/clients/codex/README.md
deleted file mode 100644
index edd5f9a..0000000
--- a/clients/codex/README.md
+++ /dev/null
@@ -1,28 +0,0 @@
-# Codex CLI
-
-## Goal
-
-Use Vibeflow n8n with Codex CLI as the behavior layer that guides intake, planning, build, validation, and final reporting.
-
-## Recommended setup idea
-
-- connect Codex CLI to the n8n MCP server
-- load the core prompt from `templates/system-prompt.md`
-- optionally keep `docs/skill-spec.md` and `docs/conversation-contract.md` in the working directory for extra context
-
-## Suggested working pattern
-
-1. Start Codex in a project folder that contains this repository.
-2. Ensure MCP access to n8n is configured.
-3. Ask for a workflow in natural language.
-4. Let the agent ask a few focused questions.
-5. Review the plan.
-6. Approve the build.
-
-## Helpful prompt seed
-
-```text
-Use the Vibeflow n8n skill in this repository. Interview me briefly, produce a build plan, then create the workflow in n8n through MCP and return a final handoff report.
-```
-
-See `config-snippets.md` for example `codex mcp add` and `config.toml` setup.
diff --git a/clients/codex/config-snippets.md b/clients/codex/config-snippets.md
deleted file mode 100644
index 4b23843..0000000
--- a/clients/codex/config-snippets.md
+++ /dev/null
@@ -1,31 +0,0 @@
-# Codex CLI config snippets
-
-These snippets are examples. Replace placeholder URLs and names with your real n8n MCP configuration.
-
-## Option 1: add with CLI
-
-```bash
-codex mcp add n8n --url https://YOUR-N8N-MCP-ENDPOINT
-codex mcp list
-```
-
-## Option 2: add in config file
-
-Project-scoped or user-scoped configuration can live in `~/.codex/config.toml` or `.codex/config.toml`.
-
-```toml
-[mcp_servers.n8n]
-url = "https://YOUR-N8N-MCP-ENDPOINT"
-```
-
-## Optional instruction in AGENTS.md
-
-```text
-Always use the n8n MCP server when the request is about creating, updating, validating, or testing n8n workflows. Before building anything, produce a normalized plan that matches schemas/plan.schema.json.
-```
-
-## Suggested first prompt
-
-```text
-Use the Vibeflow n8n skill in this repository. Interview me briefly, produce a normalized plan, then build the workflow in n8n through MCP and finish with a concise handoff report.
-```
diff --git a/clients/codex/sample-config.toml b/clients/codex/sample-config.toml
deleted file mode 100644
index 9d1b901..0000000
--- a/clients/codex/sample-config.toml
+++ /dev/null
@@ -1,10 +0,0 @@
-# Example Codex CLI MCP configuration
-# Adjust command, args, and environment to match your local n8n MCP setup.
-
-[mcp_servers.n8n]
-command = "npx"
-args = ["-y", "n8n-mcp"]
-
-[mcp_servers.n8n.env]
-N8N_BASE_URL = "http://localhost:5678"
-N8N_API_KEY = "replace-me"
diff --git a/clients/openclaude/README.md b/clients/openclaude/README.md
deleted file mode 100644
index 38030fe..0000000
--- a/clients/openclaude/README.md
+++ /dev/null
@@ -1,11 +0,0 @@
-# OpenClaude / Community Forks
-
-This target is community / best-effort.
-
-If your fork supports MCP, adapt one of the sample configurations from the primary clients and point it at your n8n MCP server.
-
-Recommended approach:
-- start from the Claude Code or OpenCode examples
-- verify your client supports local MCP servers
-- confirm environment variable names for the n8n MCP process
-- test a simple read-only operation before creating workflows
diff --git a/clients/openclaude/config-snippets.md b/clients/openclaude/config-snippets.md
deleted file mode 100644
index bd49592..0000000
--- a/clients/openclaude/config-snippets.md
+++ /dev/null
@@ -1,23 +0,0 @@
-# OpenClaude and community forks: config notes
-
-This target is best-effort.
-
-Use these guidelines only if your fork supports:
-- MCP connections
-- custom instruction or skill layers
-- conversational follow-up questions
-
-## Minimal strategy
-
-1. Point your client to the n8n MCP endpoint.
-2. Load `templates/system-prompt.md`.
-3. Keep `docs/conversation-contract.md` and `schemas/plan.schema.json` available.
-4. Test with one simple recipe before using real workflows.
-
-## Compatibility contract
-
-The client should be able to:
-- use an MCP server by name
-- ask follow-up questions
-- emit a structured plan before building
-- complete a final handoff report
diff --git a/clients/opencode/README.md b/clients/opencode/README.md
deleted file mode 100644
index f2942f9..0000000
--- a/clients/opencode/README.md
+++ /dev/null
@@ -1,21 +0,0 @@
-# OpenCode
-
-## Goal
-
-Use this repository as the instruction pack for building n8n workflows conversationally through MCP.
-
-## Suggested setup
-
-- configure the n8n MCP server in OpenCode
-- provide `templates/system-prompt.md` as the primary behavior file
-- optionally pin `docs/skill-spec.md` for extra grounding
-
-## Good defaults
-
-For most users:
-- mode: balanced
-- assumptions: explicit
-- plan required before build: yes
-- final report: concise but complete
-
-See `config-snippets.md` for example `opencode mcp add` and `opencode.jsonc` setup.
diff --git a/clients/opencode/config-snippets.md b/clients/opencode/config-snippets.md
deleted file mode 100644
index 51fa6f5..0000000
--- a/clients/opencode/config-snippets.md
+++ /dev/null
@@ -1,31 +0,0 @@
-# OpenCode config snippets
-
-These snippets are examples. Replace placeholder URLs and names with your real n8n MCP configuration.
-
-## Guided setup
-
-```bash
-opencode mcp add
-opencode mcp list
-```
-
-## Example `opencode.jsonc`
-
-```jsonc
-{
- "$schema": "https://opencode.ai/config.json",
- "mcp": {
- "n8n": {
- "type": "remote",
- "url": "https://YOUR-N8N-MCP-ENDPOINT",
- "enabled": true
- }
- }
-}
-```
-
-## Suggested agent instruction
-
-```text
-When the task is about n8n workflows, use the n8n MCP server and follow the Vibeflow n8n repository contract. Produce a normalized plan first, then build, validate, and report.
-```
diff --git a/clients/opencode/sample-opencode.jsonc b/clients/opencode/sample-opencode.jsonc
deleted file mode 100644
index 3f5a555..0000000
--- a/clients/opencode/sample-opencode.jsonc
+++ /dev/null
@@ -1,13 +0,0 @@
-{
- // Example OpenCode configuration
- "mcp": {
- "n8n": {
- "type": "local",
- "command": ["npx", "-y", "n8n-mcp"],
- "env": {
- "N8N_BASE_URL": "http://localhost:5678",
- "N8N_API_KEY": "replace-me"
- }
- }
- }
-}
diff --git a/docs/architecture.md b/docs/architecture.md
index 2df046b..b666629 100644
--- a/docs/architecture.md
+++ b/docs/architecture.md
@@ -1,160 +1,43 @@
# Architecture
-## Purpose
-
-Vibeflow n8n is a skill-first architecture for building complete n8n workflows through MCP-capable coding agents.
-
-It is not a standalone runtime.
-It is a reusable behavioral layer that can be attached to different agent clients.
-
-## High-level components
-
-### 1. User
-The human describes the automation goal in natural language.
-
-### 2. Agent client
-A CLI or coding assistant that supports:
-- custom instructions / skills,
-- MCP connections,
-- conversational follow-ups,
-- optional local file awareness.
-
-Examples:
-- Codex CLI
-- Claude Code
-- OpenCode
-- compatible community forks
-
-### 3. Vibeflow skill layer
-This repository provides the skill logic:
-- how to interview the user,
-- how to normalize requirements,
-- how to decide what must be asked,
-- how to create a plan,
-- how to validate the build,
-- how to report completion.
-
-### 4. n8n MCP server
-The execution bridge between the agent and n8n.
-
-The skill does not directly manipulate n8n internals.
-It relies on the MCP-exposed capabilities available in the connected environment.
-
-### 5. n8n instance
-The target automation environment where workflows are created, updated, and later run.
-
-## Core architecture pattern
+## Data flow
```text
-User intent
- -> Conversational intake
- -> Normalized plan
- -> MCP build actions
- -> Validation pass
- -> Human-readable handoff
+workflow JSON -> parser -> graph + parameter analysis -> findings -> text | JSON | SARIF
```
-## Why planning-first matters
+The CLI reads local files, validates their basic n8n shape, builds forward and reverse node graphs, runs deterministic policies, sorts findings, and sets an exit code from the selected threshold.
-Without a planning step, agents tend to:
-- over-assume missing requirements,
-- create brittle node graphs,
-- hide unresolved credential problems,
-- return workflows that are technically created but operationally confusing.
+## Components
-The plan acts like a blueprint before the steel beams go up.
+- `bin/vibeflow.mjs`: argument parsing, output selection, and exit codes.
+- `src/vibeflow.mjs`: configuration, file discovery, policies, graph traversal, and formatters.
+- `.vibeflow.json`: repository policy defaults.
+- `schemas/`: editor-facing configuration schema.
+- `examples/`: reproducible safe and unsafe workflows.
+- `plugins/vibeflow/`: Codex packaging; it calls the same CLI rather than duplicating policy logic.
+- `action.yml`: composite GitHub Action using the checked-in CLI.
-## Build stages
+## Policy model
-### Stage 1. Intake
-The agent captures:
-- business goal,
-- trigger,
-- systems involved,
-- desired output,
-- rules and exceptions.
+Rules have a stable ID, default severity, description, and remediation. Trusted local users may change severity, vocabulary, and banned node types. VF000 remains non-configurable because invalid input cannot be audited safely.
-### Stage 2. Requirement triage
-The agent separates:
-- critical unknowns,
-- optional detail,
-- safe defaults.
+Graph policies use the validated n8n `main` connection shape, not node order; AI resource wiring is not control flow. VF005 requires a high-confidence atomic ledger gate that emits no item for duplicate claims and dominates inbound paths to side effects. VF006 rejects any AI entry path that bypasses a positive IF check against a direct agent-status reference. VF007 requires a reachable external handoff action.
-### Stage 3. Normalized plan
-The agent produces a standard structure for execution.
-This makes behavior portable across clients.
+`--locked` rejects configuration that weakens built-in severity, changes safety vocabulary, or removes a default banned node. The GitHub Action always enables locked mode so a pull request cannot silence its own findings by editing `.vibeflow.json`.
-### Stage 4. MCP execution
-The agent creates or updates the workflow using the available MCP tools.
+## Trust boundaries
-### Stage 5. Validation
-The agent checks for:
-- broken graph structure,
-- missing dependencies,
-- unsupported assumptions,
-- absent failure branches,
-- unresolved placeholders.
+Workflow JSON is untrusted input. Vibeflow never evaluates expressions, imports workflow code, runs nodes, follows symlinks during directory discovery, or contacts URLs found in parameters. It caps files, bytes, nodes, edges, configuration vocabulary, and findings; graph traversal is iterative and linear in the validated graph.
-### Stage 6. Delivery
-The agent produces a report for the user that is operational, not ornamental.
+Output paths are selected by the caller. The GitHub Action passes inputs as quoted environment values.
-## Recommended workflow object model
+## Known limitations
-A normalized plan should include:
-- workflow_name
-- workflow_mode
-- business_goal
-- trigger
-- systems_involved
-- steps
-- branching_logic
-- data_contracts
-- credentials_required
-- risk_flags
-- error_handling
-- test_strategy
-- assumptions
-- open_questions
+- Static structure cannot prove that a condition, SQL claim, or handoff works at runtime.
+- Secret detection can miss unusual key names and can produce false positives.
+- Community nodes unknown to the side-effect catalog need explicit policy additions.
+- Locked mode protects policy content, but repository owners must still review changes to the CI workflow itself.
+- Runtime credentials, permissions, network controls, and n8n version compatibility remain outside the report.
-## Modes
-
-### fast
-Prototype-first mode.
-Uses more defaults and fewer follow-up questions.
-
-### balanced
-Default mode.
-Good mix of speed and operational sanity.
-
-### safe
-More explicit approvals, stronger validation, fewer silent assumptions.
-
-## Portability strategy
-
-The repository is intentionally text-first.
-That means:
-- prompts are markdown,
-- rules are markdown,
-- examples are markdown,
-- client-specific notes are lightweight.
-
-This keeps the project easy to adapt across agent ecosystems without locking it to a single vendor format.
-
-## Non-goals
-
-This repository does not try to:
-- replace n8n documentation,
-- replace the n8n MCP server,
-- become a full workflow execution engine,
-- hide MCP limitations,
-- generate production security posture automatically.
-
-## Future architecture extensions
-
-Potential future additions:
-- schema-driven plan JSON
-- linter rules for anti-pattern detection
-- recipe loader / scenario packs
-- test case generator
-- workflow diff summarizer
-- upgrade assistant for existing workflows
+These ceilings are deliberate. Add runtime integration only when real users demonstrate that static exports are insufficient.
diff --git a/docs/brand-kit.md b/docs/brand-kit.md
deleted file mode 100644
index 77b16bf..0000000
--- a/docs/brand-kit.md
+++ /dev/null
@@ -1,37 +0,0 @@
-# Brand kit
-
-## Project name
-
-**Vibeflow n8n**
-
-## One-line description
-
-Build complete n8n workflows through MCP with any coding agent.
-
-## Tagline options
-
-- From idea to workflow, fast.
-- Describe the automation. Let the agent build it.
-- Vibe code your n8n workflows through MCP.
-
-## Short elevator pitch
-
-Vibeflow n8n is a skill-first open-source kit that helps MCP-capable coding agents plan, build, validate, and hand off complete n8n workflows from natural-language requests.
-
-## Voice
-
-- practical
-- sharp
-- builder-friendly
-- low-ceremony
-- transparent about limits
-
-## Suggested GitHub About
-
-**Description**
-
-Build complete n8n workflows through MCP with any coding agent.
-
-**Topics**
-
-`n8n`, `mcp`, `automation`, `ai-agents`, `workflow-automation`, `codex`, `claude-code`, `opencode`, `vibe-coding`
diff --git a/docs/codex-for-oss-application.md b/docs/codex-for-oss-application.md
new file mode 100644
index 0000000..d2c3211
--- /dev/null
+++ b/docs/codex-for-oss-application.md
@@ -0,0 +1,53 @@
+# Codex for Open Source application gate
+
+Application:
+
+## Current position
+
+Vibeflow is public, MIT-licensed, owned by its principal maintainer, and aligned with Codex maintainer workflows. Version 0.8.0 establishes active engineering evidence but does not manufacture adoption.
+
+Do not submit until the live evidence section is refreshed and contains external usage.
+
+## Practical submission gate
+
+These are internal quality targets, not official OpenAI thresholds:
+
+- a tagged public release with green CI;
+- at least three unrelated external users or teams with verifiable feedback;
+- at least one external issue, discussion, or pull request with maintainer activity;
+- current traffic, clone, installation, or dependent-project evidence;
+- no confidential information in the application.
+
+## Live evidence
+
+Refresh immediately before submission:
+
+- GitHub stars: `[refresh]`
+- forks: `[refresh]`
+- unique clones in the latest available period: `[refresh]`
+- releases and latest release date: `[refresh]`
+- external contributors: `[refresh]`
+- external users or public references: `[refresh]`
+- maintainer examples: `[refresh issues, reviews, and releases]`
+
+## Form draft
+
+### Role
+
+Principal maintainer.
+
+### Why is this repository eligible?
+
+> Vibeflow is an MIT-licensed safety gate for AI-generated n8n workflows. It catches embedded secrets, unsafe webhooks, missing kill switches, human handoffs, idempotency, error paths, timeouts, and risky retries before deployment. It is maintained as a dependency-free CLI, GitHub Action, and Codex plugin. [Add refreshed external usage evidence before submitting.]
+
+### How will API credits be used?
+
+> Credits will support OSS maintenance: generate adversarial workflow fixtures, run reproducible policy evaluations, review contributed rules in pull requests, explain regressions, and prepare release reports. They will not fund a hosted commercial runtime or process private customer workflows.
+
+### Anything else?
+
+> Vibeflow comes from production lessons operating conversational automations in Brazil. It is deliberately interoperable with n8n and existing MCP tooling: it does not replace builders, it checks their output. The project ships without telemetry and keeps workflow analysis local.
+
+## Final verification
+
+Before submitting, confirm the GitHub profile and repository are public, replace every `[refresh]` marker, verify each form answer remains under 500 characters, and use accurate current evidence only.
diff --git a/docs/community-onboarding.md b/docs/community-onboarding.md
deleted file mode 100644
index ac17b0a..0000000
--- a/docs/community-onboarding.md
+++ /dev/null
@@ -1,32 +0,0 @@
-# Community onboarding
-
-This document helps new contributors land softly and find meaningful first steps.
-
-## Good first areas
-
-- improve examples and walkthroughs
-- add recipes for new automation domains
-- polish client-specific setup docs
-- add social preview assets and screenshots
-- improve validation and plan schema examples
-
-## Suggested labels
-
-- `good first issue`
-- `help wanted`
-- `documentation`
-- `recipes`
-- `client:codex`
-- `client:claude-code`
-- `client:opencode`
-- `client:openclaude`
-- `design`
-- `release`
-
-## First contribution ideas
-
-- add one new recipe with a real-world brief
-- improve one client setup guide
-- translate part of the docs
-- add one README screenshot or GIF guide
-- refine the final report template
diff --git a/docs/conversation-contract.md b/docs/conversation-contract.md
deleted file mode 100644
index 4bfe094..0000000
--- a/docs/conversation-contract.md
+++ /dev/null
@@ -1,106 +0,0 @@
-# Conversation Contract
-
-## Purpose
-
-Define how the agent should conduct the conversation before, during, and after building a workflow in n8n through MCP.
-
-## Primary rule
-
-The agent should ask as little as possible, but not less than the workflow requires.
-
-## Intake sequence
-
-### Step 1. Capture the goal
-The first objective is to understand what the workflow is meant to achieve.
-
-The user may describe:
-- a business outcome,
-- a trigger event,
-- a sequence of actions,
-- a pain point,
-- or a half-formed idea.
-
-The agent should convert that into a rough workflow shape.
-
-### Step 2. Identify critical unknowns
-The agent should decide which missing answers materially affect architecture.
-
-Examples of critical unknowns:
-- trigger type,
-- source system,
-- destination system,
-- duplicate handling,
-- approval requirements,
-- customer-facing communication,
-- destructive actions.
-
-### Step 3. Ignore optional fluff until later
-Do not ask for decorative detail early.
-
-Examples of low-priority detail:
-- exact message wording,
-- aesthetic naming choices,
-- optional metadata fields,
-- low-risk formatting preferences.
-
-## Recommended opening question set
-
-Use this only when needed.
-
-1. What should trigger the workflow?
-2. Which apps or systems are involved?
-3. What should happen from start to finish?
-4. Are there any rules, approvals, or exceptions I should respect?
-
-## Planning contract
-
-Before building, the agent should create a normalized planning summary containing:
-- workflow_name
-- workflow_mode
-- business_goal
-- trigger
-- systems_involved
-- steps
-- branching_logic
-- credentials_required
-- error_handling
-- assumptions
-- open_questions
-- test_strategy
-
-## Assumption policy
-
-The agent may infer safe defaults for low-risk details.
-The agent must clearly state those assumptions.
-
-The agent must not silently assume:
-- financial actions,
-- user-facing outbound communication,
-- delete or overwrite behavior,
-- approval rules,
-- legal/compliance-sensitive logic.
-
-## Build communication
-
-When moving into build mode, the agent should summarize:
-- what it believes it is building,
-- what assumptions it will use,
-- what remains unresolved.
-
-## Final handoff contract
-
-The final response should contain:
-- summary of workflow created,
-- assumptions used,
-- unresolved dependencies,
-- manual setup still required,
-- simple test instructions,
-- logical next improvements.
-
-## Bad conversation patterns to avoid
-
-- asking 10 questions when 3 are enough
-- pretending credentials exist
-- skipping the planning summary
-- hiding uncertainty
-- returning a technical success without operational clarity
diff --git a/docs/demo-assets.md b/docs/demo-assets.md
deleted file mode 100644
index 6b52aaf..0000000
--- a/docs/demo-assets.md
+++ /dev/null
@@ -1,48 +0,0 @@
-# Demo Assets Guide
-
-Use this document to prepare screenshots, terminal captures, and visual assets for launch.
-
-## Recommended visuals
-
-### 1. Hero banner
-Use `assets/hero-banner.svg` as a temporary visual until you capture a real workflow screenshot.
-
-### 2. Terminal intake flow
-Show the agent asking for:
-- the workflow goal
-- trigger type
-- apps involved
-- final action
-- exceptions or approvals
-
-### 3. Plan output
-Capture the normalized plan before the build starts.
-
-### 4. Workflow canvas
-Use `assets/workflow-demo.svg` as a placeholder, or replace it with a real n8n screenshot.
-
-### 5. Final handoff report
-Use `assets/final-report-demo.svg` as a placeholder, or capture a real final report from the agent.
-
-## Included placeholder assets
-
-- `assets/hero-banner.svg`
-- `assets/terminal-demo.svg`
-- `assets/workflow-demo.svg`
-- `assets/final-report-demo.svg`
-- `assets/social-preview.svg`
-
-## Capture tips
-
-- avoid showing secrets or real tokens
-- use short examples with familiar tools like Slack, Airtable, Gmail, HubSpot
-- prefer light, readable terminal themes
-- crop screenshots tightly
-- keep filenames predictable
-
-## Suggested README image order
-
-1. hero image or terminal screenshot
-2. plan screenshot
-3. workflow screenshot
-4. final report screenshot
diff --git a/docs/demo-script.md b/docs/demo-script.md
deleted file mode 100644
index b588a85..0000000
--- a/docs/demo-script.md
+++ /dev/null
@@ -1,64 +0,0 @@
-# Demo Script
-
-Use this script to record a short terminal demo, GIF, or narrated walkthrough.
-
-## Goal
-
-Show that Vibeflow n8n turns a plain-language request into a structured plan and a workflow build process.
-
-## Recommended demo length
-
-- 30 to 60 seconds for a GIF
-- 90 to 180 seconds for a narrated video
-
-## Suggested scenario
-
-"Build an n8n workflow that watches Slack mentions, creates a triage page in Notion, alerts the ops channel for urgent items, and stores a summary for later reporting."
-
-## Demo sequence
-
-### 1. Open the repo
-Show the key files:
-- `templates/system-prompt.md`
-- `docs/conversation-contract.md`
-- `recipes/slack-to-notion-triage.md`
-
-### 2. Start the agent
-Show the agent receiving a natural-language request.
-
-### 3. Intake questions
-Capture a short question set such as:
-- Which Slack channel or mention pattern should trigger the workflow?
-- What counts as urgent?
-- What Notion database should receive the item?
-- Should failures notify Slack or email?
-
-### 4. Plan output
-Show the normalized plan with these visible sections:
-- objective
-- trigger
-- steps
-- branching logic
-- credentials needed
-- validation plan
-
-### 5. Build handoff
-Show the agent summarizing what it is about to create in n8n.
-
-### 6. Workflow view
-Capture the n8n canvas or a placeholder workflow image.
-
-### 7. Final report
-Show a concise final handoff report listing:
-- what was created
-- assumptions used
-- missing credentials
-- how to test it
-
-## Recording tips
-
-- keep the terminal font large
-- hide secrets and account names
-- use a short but realistic workflow request
-- crop tightly and avoid idle pauses
-- prefer one clean storyline over many cuts
diff --git a/docs/demo.md b/docs/demo.md
new file mode 100644
index 0000000..e47b5fe
--- /dev/null
+++ b/docs/demo.md
@@ -0,0 +1,32 @@
+# Reproducible demo
+
+## Safe workflow
+
+```bash
+node bin/vibeflow.mjs check examples/safe-support-agent.workflow.json
+```
+
+Expected result: exit `0`, zero findings.
+
+## Unsafe workflow
+
+```bash
+node bin/vibeflow.mjs check examples/unsafe-support-agent.workflow.json --fail-on never
+```
+
+Expected result: VF001-VF009 findings covering secrets, dangerous nodes, webhook authentication, error handling, idempotency, AI safety, timeouts, and retries.
+
+## Automation output
+
+```bash
+node bin/vibeflow.mjs check examples/unsafe-support-agent.workflow.json --format json --fail-on never
+node bin/vibeflow.mjs check examples/unsafe-support-agent.workflow.json --format sarif --output vibeflow.sarif --fail-on never
+```
+
+For pull requests or other untrusted checkouts, add `--locked`. The bundled GitHub Action does this automatically.
+
+## Full project gate
+
+```bash
+npm run verify
+```
diff --git a/docs/first-issues.md b/docs/first-issues.md
deleted file mode 100644
index 09c7ce2..0000000
--- a/docs/first-issues.md
+++ /dev/null
@@ -1,31 +0,0 @@
-# First issues to open after launch
-
-## 1. Add CRM lead enrichment recipe
-
-**Label suggestions:** `good first issue`, `recipes`
-
-Add a recipe for lead enrichment using webhook input, enrichment step, CRM upsert, and Slack notification on failure.
-
-## 2. Add screenshots to README
-
-**Label suggestions:** `good first issue`, `documentation`, `design`
-
-Capture and add 2 to 4 screenshots or GIFs showing setup and a sample workflow result.
-
-## 3. Expand Claude Code setup guide
-
-**Label suggestions:** `good first issue`, `client:claude-code`, `documentation`
-
-Add a more detailed example for project-scoped MCP configuration and prompt loading.
-
-## 4. Add Portuguese quickstart page
-
-**Label suggestions:** `good first issue`, `documentation`
-
-Create a dedicated PT-BR quickstart page instead of keeping onboarding only inside the README.
-
-## 5. Add plan validation examples
-
-**Label suggestions:** `help wanted`, `schemas`
-
-Contribute passing and failing examples for `schemas/plan.schema.json`.
diff --git a/docs/getting-started.md b/docs/getting-started.md
deleted file mode 100644
index eaff159..0000000
--- a/docs/getting-started.md
+++ /dev/null
@@ -1,72 +0,0 @@
-# Getting Started
-
-This is the fastest path from zero to first workflow.
-
-## 1. Pick a client
-
-Recommended order:
-- Codex CLI
-- Claude Code
-- OpenCode
-
-If you are testing community forks, start with OpenClaude only after one of the primary clients is working.
-
-## 2. Make the repository available to the agent
-
-Minimum useful context:
-- `templates/system-prompt.md`
-- `docs/conversation-contract.md`
-- `schemas/plan.schema.json`
-
-Good default:
-- clone this repo in the same working directory as your automation project.
-
-## 3. Connect the client to n8n via MCP
-
-Use the client-specific notes in `clients/` and keep the n8n MCP endpoint, auth method, and environment variables outside the core prompt.
-
-## 4. Start with one recipe
-
-Use one of the examples in `recipes/` instead of jumping straight into a production-critical workflow.
-
-Recommended first runs:
-- support triage
-- lead triage
-- invoice reminder
-
-## 5. Enforce the plan-first contract
-
-Before the agent creates anything in n8n, it should produce a normalized plan with:
-- objective
-- trigger
-- systems involved
-- steps
-- branching logic
-- credentials needed
-- assumptions
-- validation plan
-
-Use `schemas/plan.schema.json` as the format contract.
-
-## 6. Approve the build
-
-After reviewing the plan, let the agent:
-- create or update the workflow in n8n
-- name nodes clearly
-- add placeholders when credentials are missing
-- return a final report
-
-## 7. Run the first test
-
-Your first test should confirm:
-- the trigger is reachable or scheduled correctly
-- each branch is connected
-- missing credentials are listed clearly
-- error paths are explicit
-- output nodes match the intended business result
-
-## First prompt to try
-
-```text
-Use the Vibeflow n8n skill in this repository. Interview me briefly, produce a normalized plan that matches the schema, then build the workflow in n8n through MCP and finish with a concise handoff report.
-```
diff --git a/docs/github-launch.md b/docs/github-launch.md
deleted file mode 100644
index 1151c2f..0000000
--- a/docs/github-launch.md
+++ /dev/null
@@ -1,49 +0,0 @@
-# GitHub Launch Pack
-
-## Suggested repository name
-
-- `vibeflow-n8n`
-
-## Suggested short description
-
-Build complete n8n workflows via MCP using coding agents like Codex CLI, Claude Code, and OpenCode.
-
-## Suggested tagline
-
-Planning-first n8n workflow generation for MCP-capable coding agents.
-
-## Suggested topics
-
-- n8n
-- mcp
-- model-context-protocol
-- automation
-- ai-agents
-- codex
-- claude-code
-- opencode
-- workflow
-- vibe-coding
-
-## Suggested social preview headline
-
-Turn natural-language automation requests into usable n8n workflows.
-
-## Suggested release title
-
-`v0.4.0 - Launch-ready release with repo copy, sample configs, and demo assets`
-
-## Suggested release notes
-
-Vibeflow n8n v0.4.0 is the first launch-ready release of the project.
-
-Highlights:
-- polished public-facing README
-- launch-day checklist
-- reusable release notes template
-- demo assets guide
-- sample config files for supported clients
-- stronger open-source positioning
-
-Recommended next step:
-Publish the repository, attach 1 to 3 screenshots, and test one recipe end-to-end.
diff --git a/docs/install.md b/docs/install.md
deleted file mode 100644
index 38334db..0000000
--- a/docs/install.md
+++ /dev/null
@@ -1,47 +0,0 @@
-# Install and Setup
-
-This document gives practical setup guidance for each supported client. Keep credentials and secret values out of the repository whenever possible.
-
-## Shared prerequisites
-
-Before setting up a client, make sure you have:
-- a working n8n instance
-- access to its MCP server or MCP-enabled endpoint
-- a client that supports MCP
-- a place to store client-level config and auth safely
-
-## Recommended setup pattern
-
-1. Configure the n8n MCP server in the client.
-2. Keep this repository in the working directory.
-3. Reference `templates/system-prompt.md` and `docs/conversation-contract.md`.
-4. Ask the agent to emit a normalized plan before it builds.
-5. Test with one recipe.
-
-## Codex CLI
-
-See `clients/codex/config-snippets.md`.
-
-## Claude Code
-
-See `clients/claude-code/config-snippets.md`.
-
-## OpenCode
-
-See `clients/opencode/config-snippets.md`.
-
-## OpenClaude and forks
-
-See `clients/openclaude/config-snippets.md`.
-
-## Secrets and auth
-
-Prefer one of these patterns:
-- client-managed OAuth flow
-- environment variables referenced by the client config
-- local machine secret manager
-
-Avoid putting raw secrets in:
-- repository-tracked JSON files
-- prompt templates
-- examples intended for public publication
diff --git a/docs/launch-assets.md b/docs/launch-assets.md
deleted file mode 100644
index 3c3ddf5..0000000
--- a/docs/launch-assets.md
+++ /dev/null
@@ -1,54 +0,0 @@
-# Launch Assets Pack
-
-## Repository metadata
-
-### Name
-`vibeflow-n8n`
-
-### Short description
-Build complete n8n workflows via MCP using coding agents like Codex CLI, Claude Code, and OpenCode.
-
-### Tagline
-Planning-first n8n workflow generation for MCP-capable coding agents.
-
-### Topics
-- n8n
-- mcp
-- model-context-protocol
-- automation
-- ai-agents
-- codex
-- claude-code
-- opencode
-- workflow
-- vibe-coding
-
-## Social post draft
-
-Launching **Vibeflow n8n** βοΈ
-
-An open-source skill kit for building complete **n8n workflows via MCP** with coding agents like **Codex CLI**, **Claude Code**, and **OpenCode**.
-
-It helps agents:
-- ask only the questions that matter
-- create a normalized plan first
-- build or update the workflow in n8n
-- return a clean handoff with assumptions and test steps
-
-Built for practical, planning-first vibe coding.
-
-## GitHub release title
-
-`v0.4.0 - Launch-ready release with repo copy, sample configs, and demo assets`
-
-## GitHub release summary
-
-Vibeflow n8n v0.4.0 is the first launch-ready release of the project.
-
-Highlights:
-- public-facing README polish
-- launch-day checklist
-- demo assets guide
-- reusable release notes
-- sample config files for supported clients
-- improved project positioning for open-source adoption
diff --git a/docs/launch-day-checklist.md b/docs/launch-day-checklist.md
deleted file mode 100644
index 1689d22..0000000
--- a/docs/launch-day-checklist.md
+++ /dev/null
@@ -1,38 +0,0 @@
-# Launch-Day Checklist
-
-## Before publishing
-
-- confirm repository name
-- confirm public vs private visibility
-- validate `README.md` renders correctly on GitHub
-- confirm all internal links work
-- confirm `VERSION` and `CHANGELOG.md` match
-- verify license choice
-- verify sample configs do not contain secrets
-- test at least one recipe on one primary client
-- add 1 to 3 screenshots or terminal captures
-- prepare first release notes
-
-## Recommended publish order
-
-1. create the GitHub repository
-2. push the default branch
-3. configure repository description and topics
-4. upload social preview image
-5. pin one usage example in the README
-6. publish release `v0.4.0`
-7. post a short launch note on social platforms or communities
-
-## After publishing
-
-- open 2 to 3 starter issues labeled `good first issue`
-- add a discussion or feedback thread
-- watch first user setup pain points
-- collect examples from early users
-- plan `v0.5.0` based on real friction
-
-## Nice extras
-
-- record a 30 to 60 second terminal demo
-- add a GIF to the README
-- include before/after screenshots of manual vs agentic workflow creation
diff --git a/docs/launch.md b/docs/launch.md
new file mode 100644
index 0000000..17a7423
--- /dev/null
+++ b/docs/launch.md
@@ -0,0 +1,16 @@
+# Launch checklist
+
+## Release gate
+
+- `npm run verify` passes on Node.js 20, 22, and 24.
+- Official skill and plugin validators pass.
+- Safe fixture exits 0; unsafe fixture exits 1.
+- SARIF is valid JSON and uploaded by CI.
+- Repository description and topics match the new product.
+- `v0.8.0` release notes match `CHANGELOG.md`.
+
+## Announcement
+
+> Vibeflow is now an executable safety gate for AI-generated n8n workflows. It checks exported JSON for secrets, exposed webhooks, missing kill switches and handoffs, idempotency, failure paths, timeouts, and unsafe retries. It is dependency-free, runs locally or in GitHub Actions, and includes a Codex plugin.
+
+Link to the repository and the safe/unsafe demo. Ask users for anonymized false-positive cases and real workflow fixtures, not stars alone.
diff --git a/docs/product-brief.md b/docs/product-brief.md
new file mode 100644
index 0000000..d9ce161
--- /dev/null
+++ b/docs/product-brief.md
@@ -0,0 +1,47 @@
+# Product brief
+
+## Product
+
+Vibeflow is an open-source safety and contract gate for exported n8n workflows, especially customer-facing workflows produced or edited by coding agents.
+
+## Problem
+
+Natural-language workflow generation is now common. The remaining failure is operational: a structurally valid workflow can still leak a credential, answer while disabled, duplicate a side effect, lack a human fallback, retry unsafely, or run forever.
+
+Existing builders and MCP servers should keep building. Vibeflow checks the result before production.
+
+## Primary users
+
+- n8n maintainers reviewing generated workflow changes;
+- automation consultancies shipping customer-facing agents;
+- platform teams enforcing workflow policy in pull requests;
+- coding agents that need deterministic feedback rather than another prompt.
+
+## Promise
+
+Given an exported workflow, produce a reproducible pass/fail report with concrete remediation and no network access.
+
+## Version 0.8 scope
+
+- dependency-free Node.js CLI;
+- text, JSON, and SARIF output;
+- configurable VF000-VF009 policies;
+- safe and unsafe fixtures;
+- GitHub Action;
+- Codex skill and plugin package.
+
+## Non-goals
+
+- generating workflows;
+- replacing n8n, n8n-mcp, or workflow-as-code tools;
+- executing or mutating live workflows;
+- proving runtime correctness;
+- hosting a SaaS dashboard.
+
+## Differentiation
+
+Vibeflow starts with policies learned from real conversational-agent operations: an off switch before inference, human handoff after low-confidence output, duplicate-event protection, explicit error paths, bounded retries, and trust-boundary hygiene.
+
+## Evidence gate
+
+The project is ready to launch when its own safe fixture passes, unsafe fixture fails, plugin validates, package packs, and CI is green. It becomes a strong Codex for Open Source candidate only after external usage and maintainer work are visible.
diff --git a/docs/publishing-guide.md b/docs/publishing-guide.md
deleted file mode 100644
index cc87e08..0000000
--- a/docs/publishing-guide.md
+++ /dev/null
@@ -1,106 +0,0 @@
-# Publishing Guide
-
-## 1. Choose the repo shape
-
-Recommended public repository name:
-
-- `vibeflow-n8n`
-
-## 2. Add baseline project files
-
-Recommended extras:
-
-- `LICENSE`
-- `.gitignore`
-- `CONTRIBUTING.md`
-- `CHANGELOG.md`
-- `SECURITY.md`
-
-## 3. First release scope
-
-Keep v1 small and useful:
-
-- one main system prompt,
-- one conversation contract,
-- one delivery template,
-- examples for common workflow requests,
-- setup snippets for Codex, Claude Code, and OpenCode.
-
-## 4. What to show in the README
-
-The homepage should answer quickly:
-
-- What is this?
-- Who is it for?
-- How does it work?
-- Which clients are supported?
-- How do I install it?
-- How do I use it?
-- What are the limitations?
-
-## 5. Suggested release roadmap
-
-### v0.1.0
-- public repo
-- core prompt and docs
-- 3 examples
-
-### v0.2.0
-- client-specific setup guides
-- stronger validation checklist
-- workflow naming conventions
-
-### v0.3.0
-- recipe library
-- vertical templates: support, CRM, AI agents, finance ops
-
-### v1.0.0
-- stable docs
-- broad examples
-- community contribution guide
-
-## 6. Community strategy
-
-Useful GitHub labels:
-
-- `good first issue`
-- `template-request`
-- `client-support`
-- `docs`
-- `examples`
-- `bug`
-- `enhancement`
-
-## 7. Good demo ideas
-
-Use examples that are instantly understandable:
-
-- Typeform -> AI summary -> Slack -> Airtable
-- Gmail -> classify with AI -> route to Notion
-- Webhook -> validate -> score -> HubSpot
-- Schedule -> fetch API -> transform -> Google Sheets
-
-## 8. Keep the promise narrow
-
-Do not promise that the agent can fully solve:
-
-- missing credentials,
-- closed-source app quirks,
-- broken third-party APIs,
-- runtime approvals/human loops through MCP-triggered execution.
-
-## 9. Badges you may want
-
-- License
-- Release
-- Docs status
-- MCP compatible
-- n8n compatible
-
-## 10. Launch checklist
-
-- README clear and short
-- examples tested
-- client instructions readable
-- limitations explicit
-- sample prompts included
diff --git a/docs/readme-premium-checklist.md b/docs/readme-premium-checklist.md
deleted file mode 100644
index 661c96a..0000000
--- a/docs/readme-premium-checklist.md
+++ /dev/null
@@ -1,38 +0,0 @@
-# README Premium Checklist
-
-Use this before publishing a release or sharing the repository publicly.
-
-## Above the fold
-
-- project name is clear
-- one-line value proposition is strong
-- target users are obvious
-- at least one visual asset appears early
-
-## Conversion layer
-
-- quick start is visible without excessive scrolling
-- supported clients are easy to spot
-- top recipes are linked directly
-- install docs are linked directly
-
-## Trust layer
-
-- changelog is up to date
-- security and contributing docs exist
-- examples are easy to find
-- roadmap is honest and short
-
-## Showcase layer
-
-- hero banner present
-- at least one terminal or workflow visual
-- demo script exists
-- at least three recipes are public-facing and polished
-
-## Launch polish
-
-- release version updated
-- launch copy updated
-- repo description matches README
-- social preview ready
diff --git a/docs/real-demo-playbook.md b/docs/real-demo-playbook.md
deleted file mode 100644
index c025350..0000000
--- a/docs/real-demo-playbook.md
+++ /dev/null
@@ -1,59 +0,0 @@
-# Real Demo Playbook
-
-Use this playbook when you want a real, credible public demo instead of a static repository tour.
-
-## Goal
-
-Show a full path from plain-English request to final n8n workflow handoff in less than five minutes.
-
-## Suggested demo storyline
-
-1. Start in the terminal with a connected MCP-capable client.
-2. Paste a short brief with one trigger, one transformation, and one notification path.
-3. Let the agent ask only one to three critical follow-up questions.
-4. Show the normalized plan before the build happens.
-5. Switch to n8n and show the created workflow.
-6. End with the final implementation report.
-
-## Best demo characteristics
-
-- one trigger
-- two to five core nodes
-- one error path
-- one obvious business outcome
-- one missing credential called out cleanly
-
-## Good public demo candidates
-
-- support triage to Slack
-- invoice reminder workflow
-- lead enrichment with CRM update
-- Slack to Notion intake sorter
-
-## What to avoid
-
-- giant workflows with many credentials
-- fragile scraping flows
-- binary-heavy flows for the first public demo
-- human approval loops during the live build
-- anything that needs ten minutes of setup before value appears
-
-## Demo capture checklist
-
-- terminal font large enough to read in social clips
-- n8n canvas zoom at readable level
-- one file or pane open with the implementation report
-- timestamps or release tags hidden if they look messy
-- fake or test credentials only
-
-## Suggested timing
-
-- 30s context
-- 60s planning
-- 90s build
-- 45s n8n review
-- 30s final report and next steps
-
-## Closing line
-
-"Describe the workflow. The agent asks only what matters, builds via MCP, and hands back something usable."
diff --git a/docs/recipes-gallery.md b/docs/recipes-gallery.md
deleted file mode 100644
index 3546797..0000000
--- a/docs/recipes-gallery.md
+++ /dev/null
@@ -1,58 +0,0 @@
-# Recipes Gallery
-
-Use this page to choose the strongest recipes for screenshots, demos, and first-run adoption.
-
-## Recommended hero recipes
-
-### 1. Lead Triage
-Best for showing business routing, safe defaults, and clean branching.
-
-Use when you want to demonstrate:
-- form or CRM intake
-- qualification logic
-- routing to a sales queue
-- notifications to Slack or email
-
-### 2. Support Triage
-Best for showing operational workflows with urgency rules.
-
-Use when you want to demonstrate:
-- inbound support intake
-- priority detection
-- response classification
-- escalation paths
-
-### 3. Invoice Reminder
-Best for finance or back-office audiences.
-
-Use when you want to demonstrate:
-- schedule triggers
-- spreadsheet or database lookups
-- reminder logic
-- retry and failure handling
-
-### 4. AI Lead Enrichment
-Best for showing AI-assisted steps without becoming overly abstract.
-
-Use when you want to demonstrate:
-- enrichment APIs
-- summary generation
-- structured output fields
-- CRM updates
-
-### 5. Slack to Notion Triage
-Best for showing a workflow that feels modern and concrete in demos.
-
-Use when you want to demonstrate:
-- Slack triggers
-- content normalization
-- Notion inserts
-- urgent routing and summaries
-
-## Suggested README order
-
-1. Slack to Notion Triage
-2. AI Lead Enrichment
-3. Invoice Reminder
-
-This sequence shows speed, intelligence, and operational depth.
diff --git a/docs/release-audit.md b/docs/release-audit.md
new file mode 100644
index 0000000..c5b3b81
--- /dev/null
+++ b/docs/release-audit.md
@@ -0,0 +1,54 @@
+# Release audit β v0.8.0
+
+Date: 2026-07-22
+
+## Decision
+
+**APTO COM RESSALVAS** for the first public executable release.
+
+No blocking defect remains in the reviewed static-analysis boundary. The remaining caveats require runtime or node-specific knowledge that an exported JSON gate cannot prove.
+
+## QA evidence
+
+- 26 automated tests pass, including one safe and one intentionally unsafe workflow.
+- The safe fixture produces zero findings; the unsafe fixture produces VF001-VF009.
+- Text, JSON, and SARIF output paths are exercised.
+- Node 20, 22, and 24 are required in CI; local verification used the available Node runtime and the remote matrix is the release gate.
+- JSON/YAML parsing, package dry-run, official skill/plugin validators, `npm audit`, and `git diff --check` are part of the final gate.
+
+## Red Team
+
+The first implementation was rejected. Regression tests now cover the reproduced bypasses:
+
+- declared webhook authentication without the matching credential reference;
+- literal secrets in raw headers, URLs, expressions, pinned data, and static data;
+- disconnected, nominal, or non-gating idempotency controls;
+- parallel AI paths that bypass a kill switch, inverted or constant conditions, and decorative Code/NoOp nodes;
+- resource connections misread as control flow and fabricated connection shapes;
+- disconnected or cyclic error handling;
+- read-only HTTP requests mislabeled as handoff;
+- excessive or non-numeric timeouts and unsafe retry settings;
+- policy weakening from an untrusted checkout;
+- terminal-control injection, deep JSON, excessive nodes/edges/files/config terms, finding amplification, and quadratic traversal.
+
+At the 5,000-node limit, the corrected linear traversal completed the synthetic chain in tens of milliseconds on the development machine. Finding truncation always adds blocking VF000.
+
+## Supply chain
+
+- GitHub-owned actions are pinned to full verified commit SHAs.
+- Checkout credentials are not persisted; workflow permissions are `contents: read`.
+- Jobs have a ten-minute timeout and package installation ignores scripts.
+- The package has no runtime dependencies and uses a publish allowlist.
+- The bundled action always enables `--locked`.
+
+The first merge SHA must replace mutable self-references in the README and Codex skill before the release tag is created.
+
+## Residual limitations
+
+- Static analysis cannot prove a referenced credential exists or works.
+- SQL and IF checks are high-confidence structural evidence, not runtime execution proofs.
+- An HTTP POST labeled as a ticket or handoff may still fail or target the wrong service.
+- Unknown community nodes may require a new side-effect adapter and regression fixture.
+- Repository owners must still review changes to the CI workflow itself.
+
+The official Codex Security workspace was opened, but its setup was never submitted through the app interface; no result from that scanner is claimed here. The release decision is based on the direct QA, independent Red Team, supply-chain review, and regression evidence above.
diff --git a/docs/release-notes-template.md b/docs/release-notes-template.md
deleted file mode 100644
index 1daf69f..0000000
--- a/docs/release-notes-template.md
+++ /dev/null
@@ -1,36 +0,0 @@
-# Release Notes Template
-
-## Title
-`vX.Y.Z - one-line summary`
-
-## Summary
-
-Briefly explain what changed and why this release matters.
-
-## Highlights
-
-- item 1
-- item 2
-- item 3
-
-## Added
-
-- new files
-- new docs
-- new examples
-
-## Improved
-
-- onboarding
-- naming
-- consistency
-
-## Fixed
-
-- broken links
-- typos
-- example mismatches
-
-## Upgrade notes
-
-Mention anything existing users should review after pulling the new version.
diff --git a/docs/release-v0.7.0.md b/docs/release-v0.7.0.md
deleted file mode 100644
index a334de0..0000000
--- a/docs/release-v0.7.0.md
+++ /dev/null
@@ -1,15 +0,0 @@
-# Vibeflow n8n v0.7.0
-
-This release focuses on public-demo readiness.
-
-## Highlights
-
-- premium README refresh
-- real-demo playbook for terminal-to-n8n walkthroughs
-- two new showcase recipes
-- illustrative sample workflow export for safer local experimentation
-- extra README quality checklist for launch polish
-
-## Recommended next move
-
-Attach real screenshots or a short GIF to make the repository feel alive on first visit.
diff --git a/docs/roadmap.md b/docs/roadmap.md
index 05887d6..d9ad87a 100644
--- a/docs/roadmap.md
+++ b/docs/roadmap.md
@@ -1,34 +1,21 @@
# Roadmap
-## Current state: v0.6.0
+## 0.8.0
-V6 is designed to be showcase-ready:
-- launch and community docs are in place
-- client snippets are documented
-- normalized plan schema exists
-- demo-oriented recipes are stronger
-- placeholder visual assets are included for the first public version
+Ship the executable reset: CLI, nine configurable policies, fixtures, tests, SARIF, GitHub Action, and Codex plugin.
-## v0.7.0 ideas
+## Next release gate
-- sample workflow exports for safe local testing
-- richer setup fixtures by client
-- validation helper scripts for plan payloads
-- GIFs and screenshots from real runs
-- recipe packs for sales, support, finance, and internal ops
+Do not add another integration by default. Prioritize evidence from real workflows:
-## v0.8.0 ideas
+1. Measure false positives by rule.
+2. Accept anonymized fixtures from external users.
+3. Add a node type or policy only with a failing fixture.
+4. Improve GitHub annotations if SARIF users request it.
+5. Package for npm only when GitHub installation creates material friction.
+6. Add node-specific handoff and side-effect adapters only with adversarial safe/unsafe fixtures.
+7. Model additional atomic idempotency gates only when their duplicate path is proven to stop downstream items.
-- optional plan linter
-- workflow quality checklist by recipe type
-- starter gallery page for examples
-- benchmark prompts for comparing clients
+## Explicitly deferred
-## Long-term direction
-
-Turn Vibeflow n8n into a portable skill pack that:
-- speaks natural language with minimal friction
-- plans before building
-- works across multiple MCP-capable coding agents
-- remains understandable to non-experts
-- feels demo-friendly from the first clone
+Hosted UI, custom MCP server, live n8n mutation, workflow generation, and broad multi-agent wrappers.
diff --git a/docs/showcase-checklist.md b/docs/showcase-checklist.md
deleted file mode 100644
index 539f599..0000000
--- a/docs/showcase-checklist.md
+++ /dev/null
@@ -1,30 +0,0 @@
-# Showcase Checklist
-
-Use this checklist before sharing the repo publicly.
-
-## README
-- clear one-line value proposition
-- 1 hero visual near the top
-- 1 example prompt
-- 3 highlighted recipes
-- quick-start links visible without scrolling too far
-
-## Assets
-- hero banner present
-- social preview present
-- at least 3 demo visuals present
-- filenames are simple and predictable
-
-## Recipes
-- at least 3 recipes feel production-adjacent
-- every recipe includes trigger, steps, assumptions, and test plan
-- one recipe includes AI enrichment
-- one recipe includes support or ops logic
-- one recipe includes finance logic
-
-## Release
-- VERSION bumped
-- CHANGELOG updated
-- GitHub release notes drafted
-- topics and description ready
-- first screenshot or GIF attached
diff --git a/docs/skill-spec.md b/docs/skill-spec.md
deleted file mode 100644
index 61b85d0..0000000
--- a/docs/skill-spec.md
+++ /dev/null
@@ -1,152 +0,0 @@
-# Skill Specification
-
-## Goal
-
-Enable an AI coding agent to create, update, and validate complete n8n workflows through MCP, using a conversational intake process and a planning-first approach.
-
-## Primary user promise
-
-The user describes what they want.
-The agent asks only the minimum useful questions.
-The agent produces a plan.
-The agent builds the workflow in n8n.
-The agent returns a clear handoff report.
-
-## Inputs
-
-### Required
-- workflow goal
-
-### Usually required
-- trigger type
-- connected apps/services
-- expected final output
-
-### Sometimes required
-- schedule/frequency
-- payload schema
-- approval logic
-- credential ownership
-- fallback behavior
-- notification behavior
-- testing sample
-
-## Outputs
-
-The skill should return:
-
-1. a structured plan,
-2. a summary of assumptions,
-3. the workflow creation/update result,
-4. unresolved dependencies,
-5. a validation report,
-6. a concise next-step checklist.
-
-## Operational sequence
-
-### 1. Intake
-Start with the user goal.
-Extract likely workflow shape.
-Ask only high-impact follow-up questions.
-
-### 2. Plan
-Before touching n8n, generate a normalized plan object with:
-
-- workflow_name
-- business_goal
-- trigger
-- systems_involved
-- steps
-- branching_logic
-- data_required
-- credentials_required
-- error_handling
-- test_strategy
-- open_questions
-- assumptions
-
-### 3. Build via MCP
-Use the n8n MCP tools to:
-
-- create a new workflow or update an existing one,
-- add the correct trigger,
-- add named nodes with sensible ordering,
-- wire success and failure paths,
-- add comments/descriptions where helpful,
-- preserve placeholders where credentials or secrets are missing.
-
-### 4. Validate
-Check for:
-
-- disconnected nodes,
-- missing required fields,
-- invalid trigger assumptions,
-- absent credential references,
-- branches with no terminal behavior,
-- missing error handling where failure is likely.
-
-### 5. Deliver
-Provide:
-
-- what was created,
-- what assumptions were used,
-- what still requires manual setup,
-- how to test the workflow,
-- what upgrades would make sense next.
-
-## Guardrails
-
-### The skill must
-- prefer asking fewer but better questions,
-- distinguish between critical unknowns and optional details,
-- state assumptions explicitly,
-- avoid pretending credentials exist when they do not,
-- avoid claiming execution success without validation evidence,
-- keep the workflow understandable for humans.
-
-### The skill must not
-- over-interview the user,
-- hide missing data,
-- create needlessly complex node graphs,
-- skip the planning stage,
-- silently invent production credentials.
-
-## Defaults policy
-
-The skill may infer safe defaults for:
-
-- workflow naming,
-- timezone if supplied elsewhere in context,
-- retry behavior,
-- notification formatting,
-- low-risk field mappings,
-- common error branches.
-
-The skill must ask before assuming for:
-
-- legal/compliance-sensitive logic,
-- destructive actions,
-- billing/payment side effects,
-- approval rules,
-- CRM upsert vs create-only behavior,
-- external communications that could spam users.
-
-## Recommended modes
-
-### fast
-Minimum questions, maximum assumptions, optimized for prototypes.
-
-### balanced
-Default mode. Good production-minded assumptions with limited follow-ups.
-
-### safe
-More validation, more explicit approvals, better for real operations.
-
-## Success criteria
-
-A good run means:
-
-- the workflow structure matches the user intent,
-- the workflow is understandable,
-- missing pieces are clearly called out,
-- the user can continue from the final report without confusion.
diff --git a/docs/social-copy.md b/docs/social-copy.md
deleted file mode 100644
index e60a5a8..0000000
--- a/docs/social-copy.md
+++ /dev/null
@@ -1,29 +0,0 @@
-# Social copy
-
-## Launch post 1
-
-Today Iβm open-sourcing **Vibeflow n8n**.
-
-It is a skill-first kit for coding agents that can build complete **n8n workflows through MCP**.
-
-Instead of handcrafting every node, the agent asks for the goal, generates a plan, builds the workflow, validates it, and hands back a usable automation.
-
-Supports:
-- Codex CLI
-- Claude Code
-- OpenCode
-- MCP-capable forks
-
-## Launch post 2
-
-Built something for the vibe coders and automation builders.
-
-**Vibeflow n8n** helps MCP-capable agents turn natural-language requests into real n8n workflows.
-
-Planning, build, validation, and handoff, all inside a skill-first open-source repo.
-
-## Short post
-
-Open-sourced: **Vibeflow n8n**
-
-Build complete n8n workflows through MCP with coding agents like Codex CLI, Claude Code, and OpenCode.
diff --git a/docs/tutorial-subir-github.md b/docs/tutorial-subir-github.md
deleted file mode 100644
index b2ddf2c..0000000
--- a/docs/tutorial-subir-github.md
+++ /dev/null
@@ -1,236 +0,0 @@
-# Tutorial: como subir o Vibeflow n8n no GitHub
-
-Este guia foi feito para vocΓͺ publicar o projeto de forma prΓ‘tica, sem virar refΓ©m de um labirinto de menus.
-
-## Antes de comeΓ§ar
-
-Tenha em mΓ£os:
-
-- uma conta no GitHub
-- Git instalado no computador, se for usar terminal
-- a pasta local do projeto ou o ZIP extraΓdo
-- um nome final para o repositΓ³rio, por exemplo `vibeflow-n8n`
-
-O GitHub permite criar um novo repositΓ³rio pela interface web ou subir um projeto local pela linha de comando com GitHub CLI. A documentaΓ§Γ£o oficial cobre os dois caminhos. ξciteξturn863639search0ξturn863639search3ξturn863639search14ξ
-
-## Caminho 1: subir pelo site do GitHub + Git local
-
-### 1) Crie o repositΓ³rio vazio
-
-No GitHub:
-
-- clique no canto superior direito em **New repository**
-- escolha o nome do repositΓ³rio, por exemplo `vibeflow-n8n`
-- adicione uma descriΓ§Γ£o curta
-- escolha **Public**
-- nΓ£o marque README, `.gitignore` ou licenΓ§a, porque este projeto jΓ‘ contΓ©m esses arquivos
-- clique em **Create repository**
-
-Esses passos seguem o fluxo atual do GitHub para criaΓ§Γ£o de repositΓ³rios. ξciteξturn863639search0ξ
-
-### 2) Extraia o ZIP da V5 no seu computador
-
-Descompacte o pacote em uma pasta local. Exemplo:
-
-```bash
-unzip n8n-workflow-skill-kit-v0.5.0.zip
-cd n8n-workflow-skill-kit
-```
-
-### 3) Inicialize o Git localmente
-
-Se a pasta ainda nΓ£o for um repositΓ³rio Git:
-
-```bash
-git init
-git add .
-git commit -m "feat: launch Vibeflow n8n v0.5.0"
-```
-
-### 4) Conecte ao repositΓ³rio remoto
-
-Copie a URL do seu repositΓ³rio recΓ©m-criado e rode:
-
-```bash
-git branch -M main
-git remote add origin https://github.com/SEU_USUARIO/vibeflow-n8n.git
-git push -u origin main
-```
-
-A prΓ³pria documentaΓ§Γ£o do GitHub cobre o fluxo de adicionar cΓ³digo local a um repositΓ³rio remoto. ξciteξturn863639search9ξturn863639search19ξ
-
-## Caminho 2: subir usando GitHub CLI
-
-Se vocΓͺ usa `gh`, o caminho fica bem mais liso.
-
-### 1) Entre na pasta do projeto
-
-```bash
-cd n8n-workflow-skill-kit
-```
-
-### 2) Inicialize e faΓ§a o primeiro commit
-
-```bash
-git init
-git add .
-git commit -m "feat: launch Vibeflow n8n v0.5.0"
-```
-
-### 3) Crie e publique com `gh`
-
-```bash
-gh repo create vibeflow-n8n --public --source=. --remote=origin --push
-```
-
-O GitHub CLI documenta esse fluxo oficialmente para criar um repositΓ³rio e subir um projeto local existente. ξciteξturn863639search3ξturn863639search14ξ
-
-## Depois do push: arrumando a vitrine do repositΓ³rio
-
-### 1) Ajuste descriΓ§Γ£o e website
-
-Na pΓ‘gina principal do repositΓ³rio:
-
-- clique no Γcone de engrenagem na Γ‘rea de About
-- adicione a descriΓ§Γ£o
-- opcionalmente, adicione um site ou link de demo
-
-SugestΓ£o de descriΓ§Γ£o:
-
-```text
-Build complete n8n workflows through MCP with any coding agent.
-```
-
-### 2) Adicione topics
-
-Topics ajudam o projeto a ser encontrado. O GitHub recomenda usΓ‘-los para classificar o repositΓ³rio por assunto e finalidade. ξciteξturn863639search5ξturn863639search11ξ
-
-SugestΓ£o de topics:
-
-```text
-n8n, mcp, automation, ai-agents, codex, claude-code, opencode, workflow-automation, vibe-coding
-```
-
-### 3) Configure a social preview
-
-No GitHub:
-
-- abra **Settings**
-- procure a Γ‘rea **Social preview**
-- envie uma imagem de capa
-
-O GitHub suporta customizaΓ§Γ£o da imagem de preview social diretamente nas configuraΓ§Γ΅es do repositΓ³rio. ξciteξturn863639search2ξturn863639search8ξ
-
-### 4) FaΓ§a a primeira release
-
-Na aba principal do repositΓ³rio:
-
-- clique em **Releases**
-- clique em **Draft a new release**
-- use a tag `v0.5.0`
-- tΓtulo sugerido: `Vibeflow n8n v0.5.0`
-- cole as release notes com base em `docs/release-notes-template.md`
-- publique
-
-O fluxo de criaΓ§Γ£o de release estΓ‘ documentado pelo GitHub na Γ‘rea de releases do repositΓ³rio. ξciteξturn863639search1ξ
-
-## Ordem recomendada de publicaΓ§Γ£o
-
-Use esta sequΓͺncia:
-
-1. extraia o ZIP
-2. ajuste nome final do projeto, se quiser
-3. faΓ§a `git init`
-4. commit inicial
-5. crie o repo no GitHub
-6. push da branch `main`
-7. revise README e About
-8. adicione topics
-9. suba social preview
-10. publique a release `v0.5.0`
-11. compartilhe
-
-## Checklist de comando rΓ‘pido
-
-### Via Git puro
-
-```bash
-cd n8n-workflow-skill-kit
-git init
-git add .
-git commit -m "feat: launch Vibeflow n8n v0.5.0"
-git branch -M main
-git remote add origin https://github.com/SEU_USUARIO/vibeflow-n8n.git
-git push -u origin main
-```
-
-### Via GitHub CLI
-
-```bash
-cd n8n-workflow-skill-kit
-git init
-git add .
-git commit -m "feat: launch Vibeflow n8n v0.5.0"
-gh repo create vibeflow-n8n --public --source=. --remote=origin --push
-```
-
-## Erros comuns
-
-### O GitHub rejeitou o push porque o repositΓ³rio remoto jΓ‘ tinha arquivos
-
-Isso normalmente acontece se vocΓͺ criou README ou `.gitignore` no GitHub na hora de criar o repositΓ³rio. A saΓda mais limpa Γ© criar outro repositΓ³rio vazio, sem arquivos iniciais.
-
-### Subi arquivos sensΓveis sem querer
-
-Pare e remova imediatamente. O GitHub alerta para nΓ£o commitar segredos ou credenciais em repositΓ³rios remotos. ξciteξturn863639search4ξ
-
-### O ZIP foi extraΓdo com uma pasta a mais
-
-Entre na pasta correta antes de rodar `git init`, senΓ£o vocΓͺ publica um matrioshka de diretΓ³rios.
-
-## Texto pronto para o About do repositΓ³rio
-
-**Description**
-
-```text
-Build complete n8n workflows through MCP with any coding agent.
-```
-
-**Website**
-
-Use seu futuro site, post, demo ou deixe em branco.
-
-**Topics**
-
-```text
-n8n
-mcp
-automation
-ai-agents
-workflow-automation
-codex
-claude-code
-opencode
-vibe-coding
-```
-
-## Texto pronto para a primeira release
-
-**Tag**
-
-```text
-v0.5.0
-```
-
-**Title**
-
-```text
-Vibeflow n8n v0.5.0
-```
-
-**Summary**
-
-```text
-First public launch of Vibeflow n8n, a skill-first open-source kit for building complete n8n workflows through MCP with coding agents like Codex CLI, Claude Code, and OpenCode.
-```
-
diff --git a/examples/example-briefs.md b/examples/example-briefs.md
deleted file mode 100644
index 7b1716b..0000000
--- a/examples/example-briefs.md
+++ /dev/null
@@ -1,10 +0,0 @@
-# Example Briefs
-
-## Example 1
-Create an n8n workflow that receives leads from a webhook, scores them with AI, sends hot leads to Slack, and stores everything in HubSpot.
-
-## Example 2
-Create a workflow that runs every weekday at 8 AM, pulls yesterday's orders from an API, summarizes key metrics, and posts the summary to Microsoft Teams.
-
-## Example 3
-Create a workflow that watches Gmail for invoices, extracts key information with AI, stores structured records in Airtable, and alerts finance when the amount is above a threshold.
diff --git a/examples/example-final-report.md b/examples/example-final-report.md
deleted file mode 100644
index bf06da7..0000000
--- a/examples/example-final-report.md
+++ /dev/null
@@ -1,30 +0,0 @@
-# Example Final Report
-
-## Summary
-A workflow named `Lead Intake and Triage` was created to receive incoming lead data, score urgency using AI, send high-priority leads to Slack, and store all leads in HubSpot.
-
-## What was created
-- Webhook trigger for incoming lead payloads
-- Validation step for required lead fields
-- AI scoring and summarization step
-- Conditional branch for hot leads
-- Slack notification for hot leads
-- HubSpot create/update action
-- Generic failure notification path
-
-## Assumptions used
-- Lead uniqueness is based on email
-- Slack notifications are required only for hot leads
-- Missing optional fields do not block processing
-
-## Manual setup still required
-- Connect OpenAI credentials
-- Connect Slack credentials
-- Connect HubSpot credentials
-- Review final field mappings for HubSpot properties
-
-## How to test
-1. Send a sample payload to the webhook
-2. Confirm the AI score is produced
-3. Verify hot leads appear in Slack
-4. Verify the record is stored in HubSpot
diff --git a/examples/example-plans.md b/examples/example-plans.md
deleted file mode 100644
index 8eaf9bd..0000000
--- a/examples/example-plans.md
+++ /dev/null
@@ -1,29 +0,0 @@
-# Example Plans
-
-## Example 1 Plan
-
-- Workflow name: Lead Intake and Triage
-- Trigger: Webhook
-- Systems: Webhook, OpenAI, Slack, HubSpot
-- Steps:
- - Receive lead payload
- - Validate required fields
- - Score and summarize with AI
- - Branch on urgency
- - Notify Slack for hot leads
- - Upsert lead in HubSpot
-- Error handling:
- - Return validation error on missing fields
- - Send Slack alert on workflow failure
-
-## Example 2 Plan
-
-- Workflow name: Daily Orders Digest
-- Trigger: Schedule weekdays 08:00
-- Systems: HTTP API, Code/Transform, Microsoft Teams
-- Steps:
- - Run on schedule
- - Fetch yesterday's orders
- - Aggregate totals and deltas
- - Generate concise summary
- - Post digest to Teams
diff --git a/examples/example-walkthroughs.md b/examples/example-walkthroughs.md
deleted file mode 100644
index bf850ac..0000000
--- a/examples/example-walkthroughs.md
+++ /dev/null
@@ -1,39 +0,0 @@
-# Example Walkthroughs
-
-## Walkthrough 1: Lead qualification
-
-### User request
-Create an n8n workflow that receives a website lead, summarizes the lead using AI, classifies urgency, sends hot leads to Slack, and saves all leads in Airtable.
-
-### Agent follow-up questions
-1. What triggers the workflow: webhook, form, or CRM event?
-2. How should a hot lead be defined?
-3. Should Airtable create new records only or update existing ones?
-4. Which Slack channel should receive hot leads?
-
-### Normalized plan summary
-- trigger: webhook
-- systems: OpenAI, Slack, Airtable
-- duplicate strategy: upsert by email
-- alert rule: hot if urgency score >= 8
-- fallback: notify ops on failure
-
-### Delivery excerpt
-Workflow created with webhook trigger, AI enrichment, urgency classification, hot lead branch, Slack notification, Airtable upsert path, and failure alert stub.
-
-## Walkthrough 2: Weekly finance digest
-
-### User request
-Every Monday morning, collect unpaid invoices from the ERP, summarize totals by customer, and send the finance team a Slack digest.
-
-### Critical follow-ups
-1. What ERP or source system holds the invoice data?
-2. What timezone should Monday morning use?
-3. Should the digest include overdue aging buckets?
-4. Is Slack the only output?
-
-### Plan summary
-- trigger: schedule weekly
-- output: internal Slack digest
-- data grouping: by customer and due bucket
-- caution: no customer-facing messages
diff --git a/examples/safe-support-agent.workflow.json b/examples/safe-support-agent.workflow.json
new file mode 100644
index 0000000..d9f1d3d
--- /dev/null
+++ b/examples/safe-support-agent.workflow.json
@@ -0,0 +1,99 @@
+{
+ "name": "Safe support agent",
+ "nodes": [
+ {
+ "id": "webhook",
+ "name": "Authenticated Webhook",
+ "type": "n8n-nodes-base.webhook",
+ "parameters": { "authentication": "headerAuth", "path": "support" },
+ "credentials": { "httpHeaderAuth": { "id": "credential-reference", "name": "Webhook Header Auth" } }
+ },
+ {
+ "id": "dedupe",
+ "name": "Claim idempotency event",
+ "type": "n8n-nodes-base.postgres",
+ "onError": "continueErrorOutput",
+ "parameters": {
+ "operation": "executeQuery",
+ "query": "INSERT INTO event_ledger (event_id) VALUES ($1) ON CONFLICT (event_id) DO NOTHING RETURNING event_id",
+ "queryReplacement": "={{ $json.event_id }}"
+ },
+ "credentials": { "postgres": { "id": "credential-reference", "name": "Event Ledger" } }
+ },
+ {
+ "id": "ledger-failure",
+ "name": "Ledger failure stop",
+ "type": "n8n-nodes-base.noOp",
+ "parameters": {}
+ },
+ {
+ "id": "enabled",
+ "name": "Agent enabled kill switch",
+ "type": "n8n-nodes-base.if",
+ "parameters": { "conditions": { "boolean": [{ "value1": "={{ $json.agent_enabled }}", "value2": true }] } }
+ },
+ {
+ "id": "agent",
+ "name": "AI Agent",
+ "type": "@n8n/n8n-nodes-langchain.agent",
+ "parameters": { "text": "={{ $json.body.message }}" }
+ },
+ {
+ "id": "model",
+ "name": "OpenAI Chat Model",
+ "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
+ "parameters": { "modelName": "gpt-4.1-mini" },
+ "credentials": { "openAiApi": { "id": "credential-reference", "name": "OpenAI" } }
+ },
+ {
+ "id": "confidence",
+ "name": "Confidence gate",
+ "type": "n8n-nodes-base.if",
+ "parameters": { "conditions": { "number": [{ "value1": "={{ $json.confidence }}", "operation": "smaller", "value2": 0.7 }] } }
+ },
+ {
+ "id": "handoff",
+ "name": "Human handoff ticket",
+ "type": "n8n-nodes-base.slack",
+ "onError": "continueErrorOutput",
+ "parameters": { "channel": "support", "text": "={{ $json.summary }}" }
+ },
+ {
+ "id": "handoff-failure",
+ "name": "Handoff failure stop",
+ "type": "n8n-nodes-base.noOp",
+ "parameters": {}
+ },
+ {
+ "id": "response",
+ "name": "Respond to Webhook",
+ "type": "n8n-nodes-base.respondToWebhook",
+ "parameters": { "respondWith": "json", "responseBody": "={{ $json }}" }
+ }
+ ],
+ "connections": {
+ "Authenticated Webhook": { "main": [[{ "node": "Claim idempotency event", "type": "main", "index": 0 }]] },
+ "Claim idempotency event": {
+ "main": [
+ [{ "node": "Agent enabled kill switch", "type": "main", "index": 0 }],
+ [{ "node": "Ledger failure stop", "type": "main", "index": 0 }]
+ ]
+ },
+ "Agent enabled kill switch": { "main": [[{ "node": "AI Agent", "type": "main", "index": 0 }], []] },
+ "OpenAI Chat Model": { "ai_languageModel": [[{ "node": "AI Agent", "type": "ai_languageModel", "index": 0 }]] },
+ "AI Agent": { "main": [[{ "node": "Confidence gate", "type": "main", "index": 0 }]] },
+ "Confidence gate": {
+ "main": [
+ [{ "node": "Human handoff ticket", "type": "main", "index": 0 }],
+ [{ "node": "Respond to Webhook", "type": "main", "index": 0 }]
+ ]
+ },
+ "Human handoff ticket": {
+ "main": [
+ [],
+ [{ "node": "Handoff failure stop", "type": "main", "index": 0 }]
+ ]
+ }
+ },
+ "settings": { "executionTimeout": 120 }
+}
diff --git a/examples/sample-plan.json b/examples/sample-plan.json
deleted file mode 100644
index 30a5633..0000000
--- a/examples/sample-plan.json
+++ /dev/null
@@ -1,105 +0,0 @@
-{
- "plan_version": "0.3.0",
- "workflow_name": "Typeform Lead Triage",
- "objective": "Capture new Typeform submissions, summarize with AI, classify lead temperature, notify Slack for hot leads, and store all submissions in Airtable.",
- "trigger": {
- "type": "typeform_submission",
- "summary": "Runs whenever a new Typeform response is received.",
- "input_shape": "Form response payload with contact, answers, and metadata."
- },
- "systems": [
- "Typeform",
- "OpenAI",
- "Slack",
- "Airtable",
- "n8n"
- ],
- "steps": [
- {
- "id": "step_1",
- "name": "Receive submission",
- "action": "Read the incoming Typeform payload."
- },
- {
- "id": "step_2",
- "name": "Summarize content",
- "action": "Generate a concise summary with AI.",
- "depends_on": [
- "step_1"
- ]
- },
- {
- "id": "step_3",
- "name": "Classify lead",
- "action": "Assign hot, warm, or cold label.",
- "depends_on": [
- "step_2"
- ]
- },
- {
- "id": "step_4",
- "name": "Branch urgent leads",
- "action": "If hot, send a Slack alert.",
- "depends_on": [
- "step_3"
- ]
- },
- {
- "id": "step_5",
- "name": "Persist record",
- "action": "Write all leads to Airtable.",
- "depends_on": [
- "step_3"
- ]
- }
- ],
- "branching": [
- {
- "condition": "lead_temperature == 'hot'",
- "path": "notify_slack"
- },
- {
- "condition": "lead_temperature != 'hot'",
- "path": "skip_slack"
- }
- ],
- "credentials": [
- {
- "system": "Typeform",
- "status": "required"
- },
- {
- "system": "OpenAI",
- "status": "required"
- },
- {
- "system": "Slack",
- "status": "required"
- },
- {
- "system": "Airtable",
- "status": "required"
- }
- ],
- "assumptions": [
- "The Typeform payload includes enough text to summarize.",
- "Slack alerting is only required for hot leads.",
- "Airtable base and table are already chosen by the user."
- ],
- "risks": [
- "Missing credentials block full end-to-end testing.",
- "Lead classification thresholds may need tuning after first runs."
- ],
- "validation": {
- "checks": [
- "Confirm the Typeform trigger fires with a real payload.",
- "Confirm AI output contains both summary and label.",
- "Confirm Slack only receives hot leads.",
- "Confirm Airtable receives every submission."
- ],
- "manual_steps": [
- "Connect credentials in n8n if placeholders were used.",
- "Review the prompt used for classification before production rollout."
- ]
- }
-}
\ No newline at end of file
diff --git a/examples/sample-workflow-export.json b/examples/sample-workflow-export.json
deleted file mode 100644
index e58a638..0000000
--- a/examples/sample-workflow-export.json
+++ /dev/null
@@ -1,57 +0,0 @@
-{
- "name": "Vibeflow Demo - Support Escalation",
- "active": false,
- "meta": {
- "source": "vibeflow-n8n",
- "note": "Illustrative export skeleton for documentation and local experimentation."
- },
- "nodes": [
- {
- "id": "Webhook_1",
- "name": "Incoming Ticket",
- "type": "n8n-nodes-base.webhook",
- "position": [260, 280],
- "parameters": {
- "path": "support-escalation-demo",
- "httpMethod": "POST"
- }
- },
- {
- "id": "Set_1",
- "name": "Normalize Payload",
- "type": "n8n-nodes-base.set",
- "position": [520, 280],
- "parameters": {
- "keepOnlySet": false,
- "values": {
- "string": [
- {"name": "ticket_id", "value": "={{$json.id || ''}}"},
- {"name": "priority", "value": "={{$json.priority || 'normal'}}"},
- {"name": "summary", "value": "={{$json.summary || ''}}"}
- ]
- }
- }
- },
- {
- "id": "If_1",
- "name": "Urgent?",
- "type": "n8n-nodes-base.if",
- "position": [780, 280],
- "parameters": {
- "conditions": {
- "string": [
- {"value1": "={{$json.priority}}", "operation": "equal", "value2": "high"}
- ]
- }
- }
- }
- ],
- "connections": {
- "Incoming Ticket": {
- "main": [[{"node": "Normalize Payload", "type": "main", "index": 0}]]
- },
- "Normalize Payload": {
- "main": [[{"node": "Urgent?", "type": "main", "index": 0}]]
- }
- }
-}
diff --git a/examples/unsafe-support-agent.workflow.json b/examples/unsafe-support-agent.workflow.json
new file mode 100644
index 0000000..405170d
--- /dev/null
+++ b/examples/unsafe-support-agent.workflow.json
@@ -0,0 +1,43 @@
+{
+ "name": "Unsafe support agent",
+ "nodes": [
+ {
+ "id": "webhook",
+ "name": "Public Webhook",
+ "type": "n8n-nodes-base.webhook",
+ "parameters": { "path": "support" }
+ },
+ {
+ "id": "agent",
+ "name": "AI Agent",
+ "type": "@n8n/n8n-nodes-langchain.agent",
+ "parameters": { "text": "Respond to the customer" }
+ },
+ {
+ "id": "api",
+ "name": "Send response",
+ "type": "n8n-nodes-base.httpRequest",
+ "retryOnFail": true,
+ "maxTries": 10,
+ "waitBetweenTries": 0,
+ "parameters": {
+ "url": "https://api.example.com/messages",
+ "headerParameters": {
+ "parameters": [{ "name": "Authorization", "value": "Bearer vf_fake_0123456789abcdef" }]
+ }
+ }
+ },
+ {
+ "id": "shell",
+ "name": "Run Shell",
+ "type": "n8n-nodes-base.executeCommand",
+ "parameters": { "command": "echo done" }
+ }
+ ],
+ "connections": {
+ "Public Webhook": { "main": [[{ "node": "AI Agent", "type": "main", "index": 0 }]] },
+ "AI Agent": { "main": [[{ "node": "Send response", "type": "main", "index": 0 }]] },
+ "Send response": { "main": [[{ "node": "Run Shell", "type": "main", "index": 0 }]] }
+ },
+ "settings": {}
+}
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..a6a7289
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,19 @@
+{
+ "name": "vibeflow-n8n",
+ "version": "0.8.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "vibeflow-n8n",
+ "version": "0.8.0",
+ "license": "MIT",
+ "bin": {
+ "vibeflow": "bin/vibeflow.mjs"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..a27923b
--- /dev/null
+++ b/package.json
@@ -0,0 +1,38 @@
+{
+ "name": "vibeflow-n8n",
+ "version": "0.8.0",
+ "description": "Safety and contract checks for AI-generated n8n workflows",
+ "type": "module",
+ "bin": {
+ "vibeflow": "./bin/vibeflow.mjs"
+ },
+ "files": [
+ "bin",
+ "src",
+ "schemas",
+ "LICENSE",
+ "README.md"
+ ],
+ "scripts": {
+ "check": "node ./bin/vibeflow.mjs check ./examples/safe-support-agent.workflow.json",
+ "test": "node --test",
+ "verify": "npm test && npm run check && npm pack --dry-run"
+ },
+ "engines": {
+ "node": ">=20"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/domfelipe/vibeflow-n8n.git"
+ },
+ "keywords": [
+ "n8n",
+ "workflow",
+ "security",
+ "lint",
+ "ai-agents",
+ "codex"
+ ],
+ "author": "Felipe Domingues",
+ "license": "MIT"
+}
diff --git a/plugins/vibeflow/.codex-plugin/plugin.json b/plugins/vibeflow/.codex-plugin/plugin.json
new file mode 100644
index 0000000..4529029
--- /dev/null
+++ b/plugins/vibeflow/.codex-plugin/plugin.json
@@ -0,0 +1,28 @@
+{
+ "name": "vibeflow",
+ "version": "0.8.0",
+ "description": "Audit and repair AI-generated n8n workflows before production",
+ "author": {
+ "name": "Felipe Domingues",
+ "url": "https://github.com/domfelipe"
+ },
+ "homepage": "https://github.com/domfelipe/vibeflow-n8n",
+ "repository": "https://github.com/domfelipe/vibeflow-n8n",
+ "license": "MIT",
+ "keywords": ["n8n", "workflow", "security", "quality", "ai-agents"],
+ "skills": "./skills/",
+ "interface": {
+ "displayName": "Vibeflow",
+ "shortDescription": "Safety checks for AI-generated n8n workflows",
+ "longDescription": "Run deterministic safety and contract checks on n8n workflow exports, explain findings, and repair blocking issues before deployment.",
+ "developerName": "Felipe Domingues",
+ "category": "Productivity",
+ "capabilities": ["Read", "Write"],
+ "websiteURL": "https://github.com/domfelipe/vibeflow-n8n",
+ "defaultPrompt": [
+ "Audit this n8n workflow before production.",
+ "Fix the blocking Vibeflow findings.",
+ "Explain the safety risks in this workflow."
+ ]
+ }
+}
diff --git a/plugins/vibeflow/skills/vibeflow/SKILL.md b/plugins/vibeflow/skills/vibeflow/SKILL.md
new file mode 100644
index 0000000..aedc652
--- /dev/null
+++ b/plugins/vibeflow/skills/vibeflow/SKILL.md
@@ -0,0 +1,40 @@
+---
+name: vibeflow
+description: Audit and repair exported n8n workflow JSON with deterministic safety and contract checks. Use when reviewing AI-generated or customer-facing n8n workflows before deployment, investigating Vibeflow VF000-VF009 findings, adding kill switches or human handoffs, checking secrets, retries, and webhook exposure, or preparing workflow changes for CI and pull requests.
+---
+
+# Vibeflow
+
+Use the CLI as the source of truth. Do not infer that a workflow is safe from its README, prompt, or visual layout.
+
+## Workflow
+
+1. Locate the exported workflow JSON. Never inspect or mutate a live production workflow unless the user explicitly requests it.
+2. Run Vibeflow from a repository checkout:
+
+ ```bash
+ node bin/vibeflow.mjs check path/to/workflow.json
+ ```
+
+ For a released version without a checkout:
+
+ ```bash
+ npx --yes github:domfelipe/vibeflow-n8n#v0.8.0 check path/to/workflow.json
+ ```
+
+3. Read [references/policies.md](references/policies.md) when interpreting or repairing a finding.
+4. Fix errors before warnings. Preserve credential references, expressions, node IDs, and unrelated workflow behavior.
+5. Re-run the check after every repair. Stop only when blocking findings are gone or explicitly waived in `.vibeflow.json` with a documented reason.
+6. Report changed nodes, remaining warnings, and what still needs runtime validation in n8n.
+
+## Safety boundaries
+
+- Never copy literal credentials into a workflow to silence `VF001`.
+- Never disable `VF006` for customer-facing agents without explicit user approval; an off switch must block inference and all AI responses.
+- Treat static analysis as a preflight, not proof of runtime correctness.
+- Prefer fixing a shared upstream node over duplicating guards across branches.
+- Keep fixes local to the exported workflow until the user authorizes deployment.
+
+## Output
+
+Return the CLI result, the smallest safe repair, and any unresolved runtime check. Use SARIF for GitHub code scanning and JSON for automation.
diff --git a/plugins/vibeflow/skills/vibeflow/agents/openai.yaml b/plugins/vibeflow/skills/vibeflow/agents/openai.yaml
new file mode 100644
index 0000000..0766502
--- /dev/null
+++ b/plugins/vibeflow/skills/vibeflow/agents/openai.yaml
@@ -0,0 +1,4 @@
+interface:
+ display_name: "Vibeflow"
+ short_description: "Safety checks for AI-generated n8n workflows"
+ default_prompt: "Use $vibeflow to audit this n8n workflow and fix blocking findings."
diff --git a/plugins/vibeflow/skills/vibeflow/references/policies.md b/plugins/vibeflow/skills/vibeflow/references/policies.md
new file mode 100644
index 0000000..7864016
--- /dev/null
+++ b/plugins/vibeflow/skills/vibeflow/references/policies.md
@@ -0,0 +1,16 @@
+# Vibeflow policy reference
+
+| ID | Default | Meaning | Minimum repair |
+|---|---|---|---|
+| VF000 | error | Invalid workflow export | Export valid n8n JSON with a `nodes` array. |
+| VF001 | error | Literal secret in parameters | Use n8n credentials, a vault, or an environment expression. |
+| VF002 | error | Host-level node | Remove it or explicitly approve the node type in policy. |
+| VF003 | warning | Webhook without supported auth and a credential reference | Add header/basic/JWT auth backed by an n8n credential. |
+| VF004 | warning | External action without a connected failure path | Connect a `continueErrorOutput` branch or configure `settings.errorWorkflow`. |
+| VF005 | warning | Inbound side effects without an atomic deduplication gate | Use a claim that emits no item for duplicates, such as `INSERT ... ON CONFLICT DO NOTHING ... RETURNING`, on every path. |
+| VF006 | error | An AI entry path bypasses the structural kill switch | Put a real IF/Switch/Code gate that reads agent status on every entry path. |
+| VF007 | warning | AI Agent without a reachable external handoff action | Add a downstream ticket, chat, email, or equivalent human escalation action. |
+| VF008 | warning | Missing or excessive execution timeout | Set the workflow timeout between 1 and 3600 seconds. |
+| VF009 | warning | Unsafe retry policy | Add idempotency, bound attempts, and configure backoff. |
+
+Configuration changes severity or domain vocabulary; it does not prove the suppressed risk is safe. Keep waivers visible in `.vibeflow.json` and explain them in the pull request.
diff --git a/recipes/ai-lead-enrichment.md b/recipes/ai-lead-enrichment.md
deleted file mode 100644
index b0ea137..0000000
--- a/recipes/ai-lead-enrichment.md
+++ /dev/null
@@ -1,48 +0,0 @@
-# Recipe: AI Lead Enrichment
-
-## Goal
-
-Capture a new inbound lead, enrich the company context, generate a short structured summary, score the lead using simple rules, and write the result to the CRM.
-
-## Typical trigger
-
-- new CRM lead
-- form submission
-- webhook from a landing page
-
-## Systems involved
-
-- CRM such as HubSpot or Pipedrive
-- enrichment source such as Clearbit-like data or internal lookup
-- optional LLM step for summarization
-- Slack for alerts
-
-## Core steps
-
-1. Receive the lead payload.
-2. Normalize fields such as company name, email domain, and source.
-3. Query enrichment data.
-4. Generate a concise structured summary.
-5. Apply a scoring rule based on company size, geography, and source.
-6. Update the CRM record.
-7. Notify Slack if the lead crosses a threshold.
-
-## Important assumptions
-
-- scoring starts with safe defaults
-- enrichment can fail without blocking the whole workflow
-- missing optional fields should not break CRM updates
-
-## What makes this recipe good for demos
-
-- shows practical AI usage
-- stays grounded in business logic
-- produces an output people immediately understand
-
-## Validation checklist
-
-- lead payload is parsed correctly
-- enrichment failures are captured clearly
-- summary format is stable
-- score thresholds are visible and editable
-- CRM update succeeds even when optional data is missing
diff --git a/recipes/customer-support-escalation.md b/recipes/customer-support-escalation.md
deleted file mode 100644
index 3e6f0a0..0000000
--- a/recipes/customer-support-escalation.md
+++ /dev/null
@@ -1,35 +0,0 @@
-# Recipe: Customer Support Escalation
-
-## Goal
-
-Create an n8n workflow that watches a support inbox or helpdesk webhook, detects priority signals, enriches the ticket context, and escalates high-risk items to Slack with a clear summary.
-
-## Why it demos well
-
-- obvious business value
-- clear trigger and escalation outcome
-- easy to explain in less than two minutes
-- one branch can demonstrate prioritization logic
-
-## Suggested steps
-
-1. Trigger from webhook or helpdesk event
-2. Normalize ticket payload
-3. Score urgency using rules or an LLM step
-4. Route high-priority items to Slack
-5. Log normal-priority items to a sheet or database
-6. Add error notifications
-
-## Critical questions
-
-- What tool sends the ticket event?
-- What counts as urgent?
-- Who should receive escalations?
-- Should every item be logged, or only urgent ones?
-
-## Safe defaults
-
-- urgency based on keywords, SLA breach flag, and VIP marker
-- Slack notification for high priority only
-- spreadsheet log for all processed tickets
-- retry once before failure alert
diff --git a/recipes/hubspot-to-slack-qualification.md b/recipes/hubspot-to-slack-qualification.md
deleted file mode 100644
index 34b0b80..0000000
--- a/recipes/hubspot-to-slack-qualification.md
+++ /dev/null
@@ -1,34 +0,0 @@
-# Recipe: HubSpot to Slack Qualification
-
-## Goal
-
-Create an n8n workflow that listens for new or updated HubSpot leads, evaluates qualification rules, and sends only promising leads to Slack with a concise summary.
-
-## Why it demos well
-
-- popular stack
-- simple trigger, filter, and notification pattern
-- easy to narrate as a sales ops use case
-
-## Suggested steps
-
-1. Trigger on lead created or updated
-2. Map company, role, source, and score fields
-3. Apply qualification rules
-4. Send qualified leads to Slack
-5. Log non-qualified leads for later review
-6. Emit failure alerts when processing breaks
-
-## Critical questions
-
-- Which HubSpot event should trigger the flow?
-- What rules define a qualified lead?
-- What Slack channel should receive alerts?
-- Should the workflow update HubSpot properties too?
-
-## Safe defaults
-
-- qualify by score threshold and role/company fit
-- send concise Slack message with source and score
-- log rejected leads to a review table
-- leave CRM write-back optional
diff --git a/recipes/invoice-reminder.md b/recipes/invoice-reminder.md
deleted file mode 100644
index 00c6bb7..0000000
--- a/recipes/invoice-reminder.md
+++ /dev/null
@@ -1,17 +0,0 @@
-# Recipe: Invoice Reminder Workflow
-
-## Scenario
-
-Track invoices nearing due date, notify the internal team, and optionally send customer reminders with explicit approval rules.
-
-## Caution
-
-This recipe should not assume customer-facing messages are allowed without confirmation.
-
-## Suggested intake questions
-
-1. Where do invoice records come from?
-2. How many days before due date should reminders fire?
-3. Is there an approval step before contacting customers?
-4. Which channels should be used for internal vs external notifications?
-5. How should paid invoices be excluded?
diff --git a/recipes/lead-triage.md b/recipes/lead-triage.md
deleted file mode 100644
index 9366b5e..0000000
--- a/recipes/lead-triage.md
+++ /dev/null
@@ -1,51 +0,0 @@
-# Recipe: Lead Triage with AI Enrichment
-
-## Scenario
-
-A user wants to capture incoming leads, enrich them with AI, classify urgency, notify a team, and store the lead in a structured system.
-
-## Typical trigger
-
-- webhook
-- form submission
-- CRM new record
-
-## Systems involved
-
-- form tool or webhook source
-- AI provider
-- Slack or email
-- Airtable / HubSpot / CRM
-
-## Suggested intake questions
-
-1. What triggers the workflow?
-2. Where should the lead be stored?
-3. How should hot leads be defined?
-4. Who should be notified and where?
-5. Should the workflow create-only or upsert existing leads?
-
-## Suggested node sequence
-
-1. Trigger
-2. Normalize payload
-3. AI enrichment / classification
-4. Conditional branch by priority
-5. Notification for high priority
-6. Store record
-7. Error notification branch
-
-## Common risks
-
-- duplicate lead creation
-- unclear hot/warm/cold criteria
-- missing CRM identifiers
-- over-broad notifications
-
-## Validation checklist
-
-- priority branch exists
-- CRM mapping is explicit
-- alert channel is defined
-- duplicate strategy is stated
-- missing credential placeholders are visible
diff --git a/recipes/slack-to-notion-triage.md b/recipes/slack-to-notion-triage.md
deleted file mode 100644
index 02e0405..0000000
--- a/recipes/slack-to-notion-triage.md
+++ /dev/null
@@ -1,47 +0,0 @@
-# Recipe: Slack to Notion Triage
-
-## Goal
-
-Capture important Slack mentions, classify them, create a Notion item for tracking, and route urgent cases to an escalation channel.
-
-## Typical trigger
-
-- mention of a bot in Slack
-- reactions on flagged messages
-- specific channel messages matching a pattern
-
-## Systems involved
-
-- Slack
-- Notion
-- optional AI classification step
-- optional email or secondary Slack alert
-
-## Core steps
-
-1. Listen for the configured Slack trigger.
-2. Normalize message text, author, channel, and timestamp.
-3. Classify urgency and category.
-4. Create a Notion page or database item.
-5. Route urgent messages to an escalation destination.
-6. Write a final status field for reporting.
-
-## Important assumptions
-
-- urgency rules should be explicit and editable
-- Notion database schema is known or mapped in advance
-- Slack formatting should be simplified before storage
-
-## What makes this recipe good for demos
-
-- looks modern and relatable
-- easy to understand visually
-- strong before-and-after story from message to tracked item
-
-## Validation checklist
-
-- trigger fires only for intended messages
-- urgency logic is deterministic
-- Notion fields are mapped correctly
-- urgent path and normal path both succeed
-- duplicate handling is defined
diff --git a/recipes/support-triage.md b/recipes/support-triage.md
deleted file mode 100644
index 32dea69..0000000
--- a/recipes/support-triage.md
+++ /dev/null
@@ -1,23 +0,0 @@
-# Recipe: Support Ticket Triage
-
-## Scenario
-
-Route support requests from a form, email parser, or webhook into categorized queues with optional AI summarization and escalation.
-
-## Suggested node sequence
-
-1. Trigger
-2. Extract or normalize ticket fields
-3. AI summary and category suggestion
-4. Severity classification
-5. Conditional routing
-6. Ticket creation or update
-7. Team notification
-8. Failure path
-
-## High-value follow-ups
-
-- What defines severity?
-- Which queues or teams should receive each category?
-- Should responses be internal-only or customer-facing?
-- Should the workflow auto-tag, auto-assign, or only recommend?
diff --git a/schemas/plan.schema.json b/schemas/plan.schema.json
deleted file mode 100644
index 242bc5f..0000000
--- a/schemas/plan.schema.json
+++ /dev/null
@@ -1,171 +0,0 @@
-{
- "$schema": "https://json-schema.org/draft/2020-12/schema",
- "$id": "https://example.com/vibeflow-n8n/schemas/plan.schema.json",
- "title": "Vibeflow n8n Plan",
- "type": "object",
- "required": [
- "plan_version",
- "objective",
- "trigger",
- "systems",
- "steps",
- "credentials",
- "assumptions",
- "validation"
- ],
- "properties": {
- "plan_version": {
- "type": "string"
- },
- "workflow_name": {
- "type": "string"
- },
- "objective": {
- "type": "string"
- },
- "trigger": {
- "type": "object",
- "required": [
- "type",
- "summary"
- ],
- "properties": {
- "type": {
- "type": "string"
- },
- "summary": {
- "type": "string"
- },
- "schedule": {
- "type": "string"
- },
- "input_shape": {
- "type": "string"
- }
- },
- "additionalProperties": true
- },
- "systems": {
- "type": "array",
- "items": {
- "type": "string"
- },
- "minItems": 1
- },
- "steps": {
- "type": "array",
- "minItems": 1,
- "items": {
- "type": "object",
- "required": [
- "id",
- "name",
- "action"
- ],
- "properties": {
- "id": {
- "type": "string"
- },
- "name": {
- "type": "string"
- },
- "action": {
- "type": "string"
- },
- "depends_on": {
- "type": "array",
- "items": {
- "type": "string"
- }
- },
- "notes": {
- "type": "string"
- }
- },
- "additionalProperties": true
- }
- },
- "branching": {
- "type": "array",
- "items": {
- "type": "object",
- "required": [
- "condition",
- "path"
- ],
- "properties": {
- "condition": {
- "type": "string"
- },
- "path": {
- "type": "string"
- }
- },
- "additionalProperties": true
- }
- },
- "credentials": {
- "type": "array",
- "items": {
- "type": "object",
- "required": [
- "system",
- "status"
- ],
- "properties": {
- "system": {
- "type": "string"
- },
- "status": {
- "type": "string",
- "enum": [
- "required",
- "optional",
- "configured",
- "placeholder"
- ]
- },
- "notes": {
- "type": "string"
- }
- },
- "additionalProperties": true
- }
- },
- "assumptions": {
- "type": "array",
- "items": {
- "type": "string"
- }
- },
- "risks": {
- "type": "array",
- "items": {
- "type": "string"
- }
- },
- "validation": {
- "type": "object",
- "required": [
- "checks"
- ],
- "properties": {
- "checks": {
- "type": "array",
- "minItems": 1,
- "items": {
- "type": "string"
- }
- },
- "manual_steps": {
- "type": "array",
- "items": {
- "type": "string"
- }
- }
- },
- "additionalProperties": true
- }
- },
- "additionalProperties": true
-}
\ No newline at end of file
diff --git a/schemas/vibeflow-config.schema.json b/schemas/vibeflow-config.schema.json
new file mode 100644
index 0000000..c653f7e
--- /dev/null
+++ b/schemas/vibeflow-config.schema.json
@@ -0,0 +1,50 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://github.com/domfelipe/vibeflow-n8n/schemas/vibeflow-config.schema.json",
+ "title": "Vibeflow configuration",
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "$schema": { "type": "string" },
+ "rules": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "VF001": { "$ref": "#/$defs/severity" },
+ "VF002": { "$ref": "#/$defs/severity" },
+ "VF003": { "$ref": "#/$defs/severity" },
+ "VF004": { "$ref": "#/$defs/severity" },
+ "VF005": { "$ref": "#/$defs/severity" },
+ "VF006": { "$ref": "#/$defs/severity" },
+ "VF007": { "$ref": "#/$defs/severity" },
+ "VF008": { "$ref": "#/$defs/severity" },
+ "VF009": { "$ref": "#/$defs/severity" }
+ }
+ },
+ "terms": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "killSwitch": { "$ref": "#/$defs/terms" },
+ "humanHandoff": { "$ref": "#/$defs/terms" },
+ "idempotency": { "$ref": "#/$defs/terms" }
+ }
+ },
+ "bannedNodeTypes": {
+ "type": "array",
+ "items": { "type": "string", "minLength": 1, "maxLength": 200 },
+ "maxItems": 1000,
+ "uniqueItems": true
+ }
+ },
+ "$defs": {
+ "severity": { "enum": ["error", "warning", "off"] },
+ "terms": {
+ "type": "array",
+ "items": { "type": "string", "minLength": 1, "maxLength": 100 },
+ "minItems": 1,
+ "maxItems": 100,
+ "uniqueItems": true
+ }
+ }
+}
diff --git a/src/vibeflow.mjs b/src/vibeflow.mjs
new file mode 100644
index 0000000..460a53a
--- /dev/null
+++ b/src/vibeflow.mjs
@@ -0,0 +1,900 @@
+import { access, readFile, readdir, stat } from "node:fs/promises";
+import path from "node:path";
+
+export const VERSION = "0.8.0";
+export const MAX_WORKFLOW_BYTES = 25 * 1024 * 1024;
+export const MAX_CONFIG_BYTES = 256 * 1024;
+export const MAX_WORKFLOW_FILES = 1_000;
+export const MAX_NODES = 5_000;
+export const MAX_EDGES = 20_000;
+export const MAX_FINDINGS_PER_FILE = 1_000;
+export const MAX_EXECUTION_TIMEOUT_SECONDS = 3_600;
+
+export const RULES = Object.freeze({
+ VF000: {
+ name: "invalid-workflow",
+ severity: "error",
+ description: "The input must be valid JSON with an n8n nodes array.",
+ remediation: "Export the workflow again and pass a valid n8n workflow JSON file.",
+ },
+ VF001: {
+ name: "embedded-secret",
+ severity: "error",
+ description: "Literal credentials must not be embedded in node parameters.",
+ remediation: "Move the value to n8n credentials, a secret store, or an environment expression.",
+ },
+ VF002: {
+ name: "dangerous-node",
+ severity: "error",
+ description: "Host-level command, SSH, and local-file nodes expand the blast radius.",
+ remediation: "Remove the node or explicitly change the bannedNodeTypes policy after review.",
+ },
+ VF003: {
+ name: "unprotected-webhook",
+ severity: "warning",
+ description: "Public webhooks should authenticate callers at the trust boundary.",
+ remediation: "Configure header or basic authentication, or document and suppress the rule.",
+ },
+ VF004: {
+ name: "missing-error-handling",
+ severity: "warning",
+ description: "External side effects need an explicit failure path.",
+ remediation: "Connect a continueErrorOutput branch or configure workflow.settings.errorWorkflow.",
+ },
+ VF005: {
+ name: "missing-idempotency",
+ severity: "warning",
+ description: "Inbound workflows with side effects need duplicate-event protection.",
+ remediation: "Use an atomic claim that emits no item for duplicates, such as INSERT ... ON CONFLICT DO NOTHING ... RETURNING.",
+ },
+ VF006: {
+ name: "missing-kill-switch",
+ severity: "error",
+ description: "Customer-facing AI paths need an upstream off switch.",
+ remediation: "Gate every AI Agent path with an enabled/status/kill-switch check before inference.",
+ },
+ VF007: {
+ name: "missing-human-handoff",
+ severity: "warning",
+ description: "AI Agent paths should expose a downstream human fallback.",
+ remediation: "Add a confidence or exception branch that creates a ticket or hands off to a human.",
+ },
+ VF008: {
+ name: "missing-timeout",
+ severity: "warning",
+ description: "AI and external-action workflows should have a bounded execution time.",
+ remediation: `Set workflow.settings.executionTimeout between 1 and ${MAX_EXECUTION_TIMEOUT_SECONDS} seconds.`,
+ },
+ VF009: {
+ name: "unsafe-retry",
+ severity: "warning",
+ description: "Retries need idempotency, bounded attempts, and backoff.",
+ remediation: "Add a deduplication guard, keep maxTries between 2 and 5, and wait at least 100 ms.",
+ },
+});
+
+const DEFAULT_CONFIG = Object.freeze({
+ rules: Object.fromEntries(Object.entries(RULES).map(([id, rule]) => [id, rule.severity])),
+ terms: {
+ killSwitch: ["agent-off", "kill switch", "agent enabled", "ai enabled", "agent status", "pause ai"],
+ humanHandoff: ["human handoff", "handoff", "human review", "escalate", "manual review", "chatwoot", "ticket"],
+ idempotency: ["idempotency", "idempotent", "dedupe", "deduplicate", "duplicate", "event ledger", "event id"],
+ },
+ bannedNodeTypes: [
+ "n8n-nodes-base.executecommand",
+ "n8n-nodes-base.ssh",
+ "n8n-nodes-base.readwritefile",
+ "n8n-nodes-base.localfiletrigger",
+ ],
+});
+
+const SIDE_EFFECT_SUFFIXES = [
+ ".httprequest",
+ ".slack",
+ ".gmail",
+ ".emailsend",
+ ".postgres",
+ ".mysql",
+ ".microsoftsql",
+ ".redis",
+ ".supabase",
+ ".stripe",
+ ".hubspot",
+ ".telegram",
+ ".twilio",
+ ".salesforce",
+ ".notion",
+ ".googlesheets",
+ ".s3",
+ ".github",
+ ".airtable",
+ ".baserow",
+ ".mongodb",
+ ".dynamodb",
+ ".googledrive",
+ ".microsoftonedrive",
+ ".microsoftoutlook",
+ ".microsoftteams",
+ ".discord",
+ ".zendesk",
+ ".freshdesk",
+ ".intercom",
+ ".servicenow",
+ ".jira",
+ ".linear",
+ ".trello",
+ ".asana",
+ ".clickup",
+ ".woocommerce",
+ ".shopify",
+ ".sendgrid",
+ ".mailchimp",
+ ".rabbitmq",
+ ".kafka",
+ ".mqtt",
+ ".graphql",
+];
+
+const HANDOFF_SUFFIXES = [
+ ".slack",
+ ".gmail",
+ ".emailsend",
+ ".telegram",
+ ".twilio",
+ ".microsoftteams",
+ ".discord",
+ ".zendesk",
+ ".freshdesk",
+ ".intercom",
+ ".servicenow",
+ ".jira",
+];
+
+const TRIGGER_SUFFIXES = [".webhook", ".formtrigger", ".chattrigger", ".telegramtrigger", ".stripetrigger"];
+const SAFE_LITERAL = /^(?:<[^>]+>|redacted|change[-_ ]?me|your[-_ ].*|example(?:[-_ ].*)?|placeholder(?:[-_ ].*)?)$/i;
+
+export async function loadConfig(explicitPath = null, cwd = process.cwd(), locked = false) {
+ let configPath = explicitPath;
+ if (!configPath) {
+ const candidate = path.join(cwd, ".vibeflow.json");
+ try {
+ await access(candidate);
+ configPath = candidate;
+ } catch {
+ return cloneDefaults();
+ }
+ }
+
+ const details = await stat(configPath);
+ if (details.size > MAX_CONFIG_BYTES) {
+ throw new Error(`Config exceeds the ${MAX_CONFIG_BYTES} byte safety limit`);
+ }
+ const raw = await readFile(configPath, "utf8");
+ let parsed;
+ try {
+ parsed = JSON.parse(raw);
+ } catch (error) {
+ throw new Error(`Invalid config JSON at ${configPath}: ${error.message}`);
+ }
+ return normalizeConfig(parsed, { locked });
+}
+
+export function normalizeConfig(input = {}, { locked = false } = {}) {
+ if (!input || typeof input !== "object" || Array.isArray(input)) {
+ throw new Error("Vibeflow config must be a JSON object");
+ }
+ const allowedKeys = new Set(["$schema", "rules", "terms", "bannedNodeTypes"]);
+ const unknownKey = Object.keys(input).find((key) => !allowedKeys.has(key));
+ if (unknownKey) throw new Error(`Unknown config key: ${unknownKey}`);
+ const config = cloneDefaults();
+
+ if (input.rules !== undefined) {
+ if (!input.rules || typeof input.rules !== "object" || Array.isArray(input.rules)) {
+ throw new Error("config.rules must be an object");
+ }
+ for (const [ruleId, severity] of Object.entries(input.rules)) {
+ if (!RULES[ruleId] || ruleId === "VF000") throw new Error(`Unknown configurable rule: ${ruleId}`);
+ if (!["error", "warning", "off"].includes(severity)) {
+ throw new Error(`Rule ${ruleId} must be error, warning, or off`);
+ }
+ if (locked && severityRank(severity) < severityRank(RULES[ruleId].severity)) {
+ throw new Error(`Locked policy cannot weaken ${ruleId}`);
+ }
+ config.rules[ruleId] = severity;
+ }
+ }
+
+ if (input.terms !== undefined) {
+ if (!input.terms || typeof input.terms !== "object" || Array.isArray(input.terms)) {
+ throw new Error("config.terms must be an object");
+ }
+ for (const key of Object.keys(input.terms)) {
+ if (!Object.hasOwn(config.terms, key)) throw new Error(`Unknown terms group: ${key}`);
+ const values = input.terms[key];
+ if (!Array.isArray(values) || values.length > 100 || values.some((value) => typeof value !== "string" || !value.trim() || value.length > 100)) {
+ throw new Error(`config.terms.${key} must contain at most 100 non-empty strings of at most 100 characters`);
+ }
+ if (locked && !sameStringSet(values, DEFAULT_CONFIG.terms[key])) {
+ throw new Error(`Locked policy cannot change config.terms.${key}`);
+ }
+ config.terms[key] = values.map((value) => value.toLowerCase());
+ }
+ }
+
+ if (input.bannedNodeTypes !== undefined) {
+ if (!Array.isArray(input.bannedNodeTypes) || input.bannedNodeTypes.length > 1_000 || input.bannedNodeTypes.some((value) => typeof value !== "string" || !value.trim() || value.length > 200)) {
+ throw new Error("config.bannedNodeTypes must contain at most 1000 non-empty strings of at most 200 characters");
+ }
+ const normalizedTypes = input.bannedNodeTypes.map((value) => value.toLowerCase());
+ if (locked && DEFAULT_CONFIG.bannedNodeTypes.some((value) => !normalizedTypes.includes(value))) {
+ throw new Error("Locked policy cannot remove default bannedNodeTypes");
+ }
+ config.bannedNodeTypes = normalizedTypes;
+ }
+
+ return config;
+}
+
+export async function checkPaths(inputs, config = cloneDefaults()) {
+ const files = await collectWorkflowFiles(inputs);
+ if (!files.length) throw new Error("No workflow files found");
+ if (files.length > MAX_WORKFLOW_FILES) {
+ throw new Error(`Input exceeds the ${MAX_WORKFLOW_FILES} workflow file safety limit`);
+ }
+
+ const results = [];
+ for (const file of files) results.push(await checkFile(file, config));
+ return buildReport(results);
+}
+
+export async function checkFile(file, config = cloneDefaults()) {
+ let workflow;
+ try {
+ const details = await stat(file);
+ if (details.size > MAX_WORKFLOW_BYTES) {
+ return {
+ file,
+ workflowName: null,
+ findings: [invalidFinding(`Workflow exceeds the ${MAX_WORKFLOW_BYTES} byte safety limit`)],
+ };
+ }
+ workflow = JSON.parse(await readFile(file, "utf8"));
+ } catch (error) {
+ return { file, workflowName: null, findings: [invalidFinding(`Invalid JSON: ${error.message}`)] };
+ }
+
+ const shapeError = validateWorkflowShape(workflow);
+ if (shapeError) return { file, workflowName: workflow?.name ?? null, findings: [invalidFinding(shapeError)] };
+
+ return {
+ file,
+ workflowName: typeof workflow.name === "string" ? workflow.name : null,
+ findings: inspectWorkflow(workflow, config),
+ };
+}
+
+export function inspectWorkflow(workflow, config = cloneDefaults()) {
+ const shapeError = validateWorkflowShape(workflow);
+ if (shapeError) return [invalidFinding(shapeError)];
+ const nodes = workflow.nodes;
+ const { adjacency, reverse } = buildGraph(workflow.connections ?? {});
+ const aiNodes = nodes.filter(isAiAgent);
+ const triggerNodes = nodes.filter(isInboundTrigger);
+ const sideEffectNodes = nodes.filter(isSideEffect);
+ const entryNames = collectEntryNames(nodes, reverse);
+ const aiNames = new Set(aiNodes.map((node) => node.name));
+ const killSwitchNames = new Set(nodes
+ .filter((node) => isKillSwitchGuard(node, config.terms.killSwitch, workflow.connections ?? {}, adjacency, aiNames))
+ .map((node) => node.name));
+ const handoffNames = nodes.filter((node) => isHumanHandoff(node, config.terms.humanHandoff)).map((node) => node.name);
+ const idempotencyNames = new Set(nodes.filter((node) => isAtomicIdempotencyGuard(node, config.terms.idempotency)).map((node) => node.name));
+ const allReachable = traverseGraph(adjacency, entryNames);
+ const reachableWithoutKillSwitch = traverseGraph(adjacency, entryNames, killSwitchNames);
+ const canReachHandoff = traverseGraph(reverse, handoffNames);
+ const triggerNames = triggerNodes.map((node) => node.name);
+ const triggerReachable = traverseGraph(adjacency, triggerNames);
+ const triggerReachableWithoutIdempotency = traverseGraph(adjacency, triggerNames, idempotencyNames);
+ const entryReachableWithoutIdempotency = traverseGraph(adjacency, entryNames, idempotencyNames);
+ const workflowHasErrorHandler = typeof workflow.settings?.errorWorkflow === "string" && workflow.settings.errorWorkflow.trim();
+ const findings = [];
+ let findingsTruncated = false;
+ const add = (ruleId, message, node = null, parameterPath = null) => {
+ const severity = config.rules[ruleId];
+ if (severity === "off" || findingsTruncated) return;
+ if (findings.length >= MAX_FINDINGS_PER_FILE) {
+ findingsTruncated = true;
+ return;
+ }
+ findings.push({
+ ruleId,
+ rule: RULES[ruleId].name,
+ severity,
+ message,
+ remediation: RULES[ruleId].remediation,
+ node: node ? { id: node.id ?? null, name: node.name ?? null, type: node.type ?? null } : null,
+ path: parameterPath,
+ });
+ };
+
+ for (const node of nodes) {
+ const secretResult = findSecretPaths(node.parameters ?? {}, MAX_FINDINGS_PER_FILE - findings.length);
+ for (const secretPath of secretResult.paths) {
+ add("VF001", `Literal secret-like value at parameters.${secretPath}`, node, `parameters.${secretPath}`);
+ }
+ if (secretResult.truncated || findingsTruncated) {
+ findingsTruncated = true;
+ break;
+ }
+
+ if (config.bannedNodeTypes.includes(lowerType(node))) {
+ add("VF002", `Node type ${node.type} is blocked by the default policy`, node);
+ }
+
+ if (lowerType(node).endsWith(".webhook")) {
+ const authIssue = webhookAuthenticationIssue(node);
+ if (authIssue) add("VF003", authIssue, node, "parameters.authentication");
+ }
+
+ if (isSideEffect(node) && !workflowHasErrorHandler && !hasConnectedErrorPath(node, workflow.connections ?? {})) {
+ add("VF004", "External-action node has no connected error output or workflow error handler", node);
+ }
+
+ if (isSideEffect(node) && node.retryOnFail === true) {
+ const maxTries = Number(node.maxTries ?? 3);
+ const waitBetweenTries = Number(node.waitBetweenTries ?? 0);
+ if (!idempotencyNames.has(node.name) && (!allReachable.has(node.name) || entryReachableWithoutIdempotency.has(node.name))) {
+ add("VF009", "Retry is enabled on a path without an atomic idempotency guard", node);
+ }
+ if (!Number.isFinite(maxTries) || maxTries < 2 || maxTries > 5) {
+ add("VF009", `Retry budget is outside the safe range: maxTries=${node.maxTries ?? "invalid"}`, node, "maxTries");
+ }
+ if (!Number.isFinite(waitBetweenTries) || waitBetweenTries < 100) {
+ add("VF009", `Retry backoff is too small: waitBetweenTries=${node.waitBetweenTries ?? 0}`, node, "waitBetweenTries");
+ }
+ }
+ }
+
+ if (!findingsTruncated) {
+ for (const section of ["pinData", "staticData"]) {
+ if (workflow[section] === undefined) continue;
+ const secretResult = findSecretPaths(workflow[section], MAX_FINDINGS_PER_FILE - findings.length);
+ for (const secretPath of secretResult.paths) {
+ const fullPath = secretPath ? `${section}.${secretPath}` : section;
+ add("VF001", `Literal secret-like value at ${fullPath}`, null, fullPath);
+ }
+ if (secretResult.truncated || findingsTruncated) {
+ findingsTruncated = true;
+ break;
+ }
+ }
+ }
+
+ if (findingsTruncated) {
+ return finalizeFindings(findings, true);
+ }
+
+ for (const sideEffectNode of sideEffectNodes) {
+ if (!idempotencyNames.has(sideEffectNode.name)
+ && triggerReachable.has(sideEffectNode.name)
+ && triggerReachableWithoutIdempotency.has(sideEffectNode.name)) {
+ add("VF005", "Inbound path reaches this external side effect without an atomic idempotency guard", sideEffectNode);
+ }
+ }
+
+ for (const aiNode of aiNodes) {
+ if (!allReachable.has(aiNode.name) || reachableWithoutKillSwitch.has(aiNode.name)) {
+ add("VF006", "At least one entry path reaches the AI Agent without a structural kill-switch gate", aiNode);
+ }
+
+ if (!canReachHandoff.has(aiNode.name)) {
+ add("VF007", "AI Agent has no reachable external-action node identified as human handoff", aiNode);
+ }
+ }
+
+ const executionTimeout = Number(workflow.settings?.executionTimeout ?? 0);
+ if ((aiNodes.length || sideEffectNodes.length)
+ && (typeof workflow.settings?.executionTimeout !== "number"
+ || !Number.isFinite(executionTimeout) || executionTimeout <= 0 || executionTimeout > MAX_EXECUTION_TIMEOUT_SECONDS)) {
+ add("VF008", `Workflow execution timeout must be between 1 and ${MAX_EXECUTION_TIMEOUT_SECONDS} seconds`, null, "settings.executionTimeout");
+ }
+
+ return finalizeFindings(findings, findingsTruncated);
+}
+
+export function formatText(report) {
+ const lines = [];
+ for (const file of report.files) {
+ const fileName = safeDisplay(file.file);
+ const workflowName = file.workflowName ? ` (${safeDisplay(file.workflowName)})` : "";
+ lines.push(`${file.findings.length ? "β" : "β"} ${fileName}${workflowName}`);
+ for (const finding of file.findings) {
+ const node = finding.node?.name ? ` [${safeDisplay(finding.node.name)}]` : "";
+ lines.push(` ${finding.severity.toUpperCase()} ${finding.ruleId}${node} ${safeDisplay(finding.message)}`);
+ lines.push(` Fix: ${finding.remediation}`);
+ }
+ }
+ lines.push("");
+ lines.push(`Checked ${report.summary.files} workflow(s): ${report.summary.errors} error(s), ${report.summary.warnings} warning(s)`);
+ return lines.join("\n");
+}
+
+export function formatJson(report) {
+ return JSON.stringify(report, null, 2);
+}
+
+export function formatSarif(report) {
+ const sarif = {
+ $schema: "https://json.schemastore.org/sarif-2.1.0.json",
+ version: "2.1.0",
+ runs: [{
+ tool: {
+ driver: {
+ name: "Vibeflow",
+ version: VERSION,
+ informationUri: "https://github.com/domfelipe/vibeflow-n8n",
+ rules: Object.entries(RULES).map(([id, rule]) => ({
+ id,
+ name: rule.name,
+ shortDescription: { text: rule.description },
+ help: { text: rule.remediation },
+ defaultConfiguration: { level: sarifLevel(rule.severity) },
+ })),
+ },
+ },
+ results: report.files.flatMap((file) => file.findings.map((finding) => ({
+ ruleId: finding.ruleId,
+ level: sarifLevel(finding.severity),
+ message: { text: `${finding.message}${finding.node?.name ? ` (node: ${finding.node.name})` : ""}` },
+ locations: [{
+ physicalLocation: {
+ artifactLocation: { uri: relativeUri(file.file) },
+ region: { startLine: 1 },
+ },
+ }],
+ }))),
+ }],
+ };
+ return JSON.stringify(sarif, null, 2);
+}
+
+function cloneDefaults() {
+ return {
+ rules: { ...DEFAULT_CONFIG.rules },
+ terms: Object.fromEntries(Object.entries(DEFAULT_CONFIG.terms).map(([key, values]) => [key, [...values]])),
+ bannedNodeTypes: [...DEFAULT_CONFIG.bannedNodeTypes],
+ };
+}
+
+async function collectWorkflowFiles(inputs) {
+ const files = new Set();
+ for (const input of inputs) {
+ const absolute = path.resolve(input);
+ let details;
+ try {
+ details = await stat(absolute);
+ } catch {
+ throw new Error(`Input not found: ${input}`);
+ }
+ if (details.isFile()) addWorkflowFile(files, absolute);
+ else if (details.isDirectory()) await collectDirectory(absolute, files);
+ else throw new Error(`Unsupported input: ${input}`);
+ }
+ return [...files].sort();
+}
+
+async function collectDirectory(directory, files) {
+ const entries = await readdir(directory, { withFileTypes: true });
+ for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
+ if ([".git", "node_modules"].includes(entry.name)) continue;
+ const absolute = path.join(directory, entry.name);
+ if (entry.isDirectory()) await collectDirectory(absolute, files);
+ else if (entry.isFile() && entry.name.endsWith(".workflow.json")) addWorkflowFile(files, absolute);
+ }
+}
+
+function addWorkflowFile(files, file) {
+ files.add(file);
+ if (files.size > MAX_WORKFLOW_FILES) {
+ throw new Error(`Input exceeds the ${MAX_WORKFLOW_FILES} workflow file safety limit`);
+ }
+}
+
+function invalidFinding(message) {
+ return {
+ ruleId: "VF000",
+ rule: RULES.VF000.name,
+ severity: "error",
+ message,
+ remediation: RULES.VF000.remediation,
+ node: null,
+ path: null,
+ };
+}
+
+function buildReport(files) {
+ const normalizedFiles = files.map((file) => ({ ...file, file: relativeUri(file.file) }));
+ const findings = normalizedFiles.flatMap((file) => file.findings);
+ return {
+ tool: { name: "vibeflow", version: VERSION },
+ files: normalizedFiles,
+ summary: {
+ files: normalizedFiles.length,
+ errors: findings.filter((finding) => finding.severity === "error").length,
+ warnings: findings.filter((finding) => finding.severity === "warning").length,
+ },
+ };
+}
+
+function buildGraph(connections) {
+ const adjacency = new Map();
+ const reverse = new Map();
+ for (const [source, outputs] of Object.entries(connections)) {
+ for (const connectionsAtIndex of outputs.main ?? []) {
+ if (!connectionsAtIndex) continue;
+ for (const connection of connectionsAtIndex) {
+ if (connection.type !== "main") continue;
+ if (!adjacency.has(source)) adjacency.set(source, new Set());
+ if (!reverse.has(connection.node)) reverse.set(connection.node, new Set());
+ adjacency.get(source).add(connection.node);
+ reverse.get(connection.node).add(source);
+ }
+ }
+ }
+ return { adjacency, reverse };
+}
+
+function traverseGraph(graph, starts, blockers = new Set()) {
+ const queue = [...starts];
+ const seen = new Set();
+ for (let cursor = 0; cursor < queue.length; cursor += 1) {
+ const name = queue[cursor];
+ if (seen.has(name) || blockers.has(name)) continue;
+ seen.add(name);
+ for (const next of graph.get(name) ?? []) queue.push(next);
+ }
+ return seen;
+}
+
+function collectEntryNames(nodes, reverse) {
+ const names = new Set(nodes.filter(isEntryTrigger).map((node) => node.name));
+ for (const node of nodes) {
+ if (!(reverse.get(node.name)?.size)) names.add(node.name);
+ }
+ return [...names];
+}
+
+function findSecretPaths(parameters, limit = MAX_FINDINGS_PER_FILE) {
+ const findings = new Set();
+ let truncated = false;
+ walk(parameters, "", (value, key, object, currentPath) => {
+ if (typeof value === "string" && ((isSensitiveKey(key) && isLiteralSecret(value)) || looksLikeEmbeddedSecret(value))) {
+ findings.add(currentPath);
+ }
+ if (object && typeof object === "object" && !Array.isArray(object)) {
+ const label = object.name ?? object.key ?? object.headerName;
+ if (typeof label === "string" && isSensitiveKey(label) && key === "value" && isLiteralSecret(value)) {
+ findings.add(currentPath);
+ }
+ }
+ if (findings.size >= limit) {
+ truncated = true;
+ return false;
+ }
+ return true;
+ });
+ return { paths: [...findings].sort(), truncated };
+}
+
+function walk(value, currentPath, visitor, parent = null) {
+ const stack = [{ value, currentPath, parent }];
+ while (stack.length) {
+ const item = stack.pop();
+ if (Array.isArray(item.value)) {
+ for (let index = item.value.length - 1; index >= 0; index -= 1) {
+ stack.push({ value: item.value[index], currentPath: `${item.currentPath}[${index}]`, parent: item.value });
+ }
+ continue;
+ }
+ if (!item.value || typeof item.value !== "object") continue;
+ const entries = Object.entries(item.value);
+ for (let index = entries.length - 1; index >= 0; index -= 1) {
+ const [key, child] = entries[index];
+ const childPath = item.currentPath ? `${item.currentPath}.${key}` : key;
+ if (visitor(child, key, item.value, childPath, item.parent) === false) return false;
+ stack.push({ value: child, currentPath: childPath, parent: item.value });
+ }
+ }
+ return true;
+}
+
+function isSensitiveKey(key) {
+ const normalized = String(key).replace(/[^a-z0-9]/gi, "").toLowerCase();
+ if (/(url|uri|name|type|id|path|field)$/.test(normalized)) return false;
+ return /(apikey|accesstoken|refreshtoken|token|password|secret|authorization)/.test(normalized);
+}
+
+function isLiteralSecret(value) {
+ const normalized = stripExpressions(value).trim();
+ return normalized.length >= 8 && !SAFE_LITERAL.test(normalized);
+}
+
+function looksLikeEmbeddedSecret(value) {
+ const original = String(value).trim();
+ if (/\bbearer\s+[a-z0-9._~+/=-]{8,}/i.test(original)
+ || /(?:access[_-]?token|refresh[_-]?token|api[_-]?key|client[_-]?secret|authorization|password)["'\s:=]+[a-z0-9._~+/=-]{8,}/i.test(original)
+ || /^[a-z][a-z0-9+.-]*:\/\/[^/\s:]+:[^@\s]{8,}@/i.test(original)) return true;
+ const normalized = stripExpressions(original).trim();
+ if (normalized.length < 8 || SAFE_LITERAL.test(normalized)) return false;
+ return /\bbearer\s+[a-z0-9._~+/=-]{8,}/i.test(normalized)
+ || /(?:access[_-]?token|refresh[_-]?token|api[_-]?key|client[_-]?secret|authorization|password)["'\s:=]+[a-z0-9._~+/=-]{8,}/i.test(normalized)
+ || /^[a-z][a-z0-9+.-]*:\/\/[^/\s:]+:[^@\s]{8,}@/i.test(normalized);
+}
+
+function stripExpressions(value) {
+ return String(value)
+ .replace(/\{\{[\s\S]*?\}\}/g, "")
+ .replace(/\$\{[^}]*\}/g, "")
+ .replace(/\$(?:env|credentials|secrets)(?:\.[a-z0-9_]+|\[[^\]]+\])/gi, "");
+}
+
+function lowerType(node) {
+ return String(node?.type ?? "").toLowerCase();
+}
+
+function isAiAgent(node) {
+ const type = lowerType(node);
+ return type.endsWith(".agent") || type.endsWith(".chainllm") || type.endsWith(".questionandanswerchain");
+}
+
+function isInboundTrigger(node) {
+ const type = lowerType(node);
+ if (TRIGGER_SUFFIXES.some((suffix) => type.endsWith(suffix))) return true;
+ if (!type.endsWith("trigger")) return false;
+ return ![".manualtrigger", ".scheduletrigger", ".errortrigger", ".executeworkflowtrigger", ".localfiletrigger"].some((suffix) => type.endsWith(suffix));
+}
+
+function isEntryTrigger(node) {
+ const type = lowerType(node);
+ return isInboundTrigger(node)
+ || [".manualtrigger", ".scheduletrigger", ".executeworkflowtrigger", ".errortrigger", ".cron", ".start"].some((suffix) => type.endsWith(suffix));
+}
+
+function isSideEffect(node) {
+ const type = lowerType(node);
+ if (SIDE_EFFECT_SUFFIXES.some((suffix) => type.endsWith(suffix))) return true;
+ const operation = String(node.parameters?.operation ?? node.parameters?.resourceOperation ?? "").toLowerCase();
+ return /^(?:create|update|delete|append|insert|upsert|send|post|publish|upload|move|copy|add|remove|executequery)$/.test(operation);
+}
+
+function webhookAuthenticationIssue(node) {
+ const authentication = String(node.parameters?.authentication ?? "none").toLowerCase();
+ if (["none", "", "undefined"].includes(authentication)) {
+ return "Webhook accepts requests without n8n authentication";
+ }
+ if (!["basicauth", "headerauth", "jwtauth"].includes(authentication)) {
+ return `Webhook uses an unrecognized authentication mode: ${safeDisplay(authentication)}`;
+ }
+ const credentials = node.credentials;
+ const expectedCredentialKeys = {
+ basicauth: ["httpbasicauth"],
+ headerauth: ["httpheaderauth"],
+ jwtauth: ["jwtauth"],
+ }[authentication];
+ const hasReference = credentials && typeof credentials === "object" && !Array.isArray(credentials)
+ && Object.entries(credentials).some(([key, reference]) => expectedCredentialKeys.includes(key.toLowerCase())
+ && reference && typeof reference === "object" && !Array.isArray(reference)
+ && [reference.id, reference.name].some((value) => typeof value === "string" && value.trim()));
+ if (!hasReference) {
+ return `Webhook declares ${authentication} but has no credential reference`;
+ }
+ return null;
+}
+
+function hasConnectedErrorPath(node, connections) {
+ if (node.onError !== "continueErrorOutput") return false;
+ const mainOutputs = connections[node.name]?.main;
+ return Array.isArray(mainOutputs) && mainOutputs.slice(1).some((output) => Array.isArray(output)
+ && output.some((connection) => reachesTerminalWithout(connection.node, node.name, connections)));
+}
+
+function reachesTerminalWithout(start, excluded, connections) {
+ const stack = [start];
+ const seen = new Set();
+ while (stack.length) {
+ const current = stack.pop();
+ if (current === excluded || seen.has(current)) continue;
+ seen.add(current);
+ const next = (connections[current]?.main ?? []).flatMap((output) => (output ?? []))
+ .filter((connection) => connection.type === "main")
+ .map((connection) => connection.node);
+ if (!next.length) return true;
+ for (const name of next) stack.push(name);
+ }
+ return false;
+}
+
+function isKillSwitchGuard(node, terms, connections, adjacency, aiNames) {
+ if (!lowerType(node).endsWith(".if") || !hasPositiveDynamicBooleanCondition(node.parameters ?? {}, terms)) return false;
+ const branches = connections[node.name]?.main;
+ if (!Array.isArray(branches) || branches.length < 2) return false;
+ const trueTargets = (branches[0] ?? []).filter((connection) => connection.type === "main").map((connection) => connection.node);
+ const falseTargets = branches.slice(1).flatMap((branch) => (branch ?? [])
+ .filter((connection) => connection.type === "main").map((connection) => connection.node));
+ const trueReachable = traverseGraph(adjacency, trueTargets);
+ const falseReachable = traverseGraph(adjacency, falseTargets);
+ return [...aiNames].some((name) => trueReachable.has(name))
+ && ![...aiNames].some((name) => falseReachable.has(name));
+}
+
+function hasPositiveDynamicBooleanCondition(parameters, terms) {
+ const stack = [parameters];
+ while (stack.length) {
+ const value = stack.pop();
+ if (Array.isArray(value)) {
+ for (const child of value) stack.push(child);
+ continue;
+ }
+ if (!value || typeof value !== "object") continue;
+ const dynamicValue = Object.values(value).find((child) => typeof child === "string"
+ && isDirectDynamicReference(child)
+ && valueContainsTerms(child, terms));
+ const operation = String(value.operation ?? "").toLowerCase().replace(/[^a-z]/g, "");
+ const positiveOperation = !operation || ["equal", "equals", "istrue", "true"].includes(operation);
+ const positiveExpectation = value.value2 === true || ["istrue", "true"].includes(operation);
+ if (dynamicValue && positiveOperation && positiveExpectation) return true;
+ for (const child of Object.values(value)) stack.push(child);
+ }
+ return false;
+}
+
+function isDirectDynamicReference(value) {
+ return /^\s*=?\{\{\s*\$(?:json|vars|env|workflow)(?:\.[a-z0-9_]+|\[['"][^'"]+['"]\])+\s*\}\}\s*$/i.test(value);
+}
+
+function isHumanHandoff(node, terms) {
+ if (!nodeContainsTerms(node, terms)) return false;
+ const type = lowerType(node);
+ if (HANDOFF_SUFFIXES.some((suffix) => type.endsWith(suffix))) return true;
+ if (type.endsWith(".httprequest")) {
+ return ["POST", "PUT", "PATCH"].includes(String(node.parameters?.method ?? "GET").toUpperCase());
+ }
+ return false;
+}
+
+function isAtomicIdempotencyGuard(node, terms) {
+ if (!nodeContainsTerms(node, terms)) return false;
+ const type = lowerType(node);
+ const parameters = node.parameters ?? {};
+ return type.endsWith(".postgres")
+ && nodeContainsSqlStatement(parameters, /^\s*insert\b[\s\S]*\bon\s+conflict\b[\s\S]*\bdo\s+nothing\b[\s\S]*\breturning\b/i);
+}
+
+function nodeContainsSqlStatement(value, pattern) {
+ let matches = false;
+ walk({ value }, "", (child) => {
+ if (typeof child !== "string") return true;
+ const withoutComments = child.replace(/\/\*[\s\S]*?\*\//g, " ").replace(/--[^\r\n]*/g, " ");
+ const withoutLiterals = withoutComments
+ .replace(/\$\$[\s\S]*?\$\$/g, "''")
+ .replace(/'(?:''|[^'])*'/g, "''")
+ .replace(/"(?:""|[^"])*"/g, '""');
+ const firstStatement = withoutLiterals.split(";", 1)[0];
+ if (pattern.test(firstStatement)) {
+ matches = true;
+ return false;
+ }
+ return true;
+ });
+ return matches;
+}
+
+function nodeContainsTerms(node, terms) {
+ return valueContainsTerms([node.name ?? "", node.type ?? "", node.parameters ?? {}], terms);
+}
+
+function valueContainsTerms(input, terms) {
+ const normalize = (value) => value.toLowerCase().replace(/[_-]+/g, " ").replace(/\s+/g, " ");
+ const needles = terms.map((term) => normalize(term));
+ const stack = [input];
+ while (stack.length) {
+ const value = stack.pop();
+ if (typeof value === "string") {
+ const normalized = normalize(value);
+ if (needles.some((term) => normalized.includes(term))) return true;
+ } else if (Array.isArray(value)) {
+ for (const child of value) stack.push(child);
+ } else if (value && typeof value === "object") {
+ for (const child of Object.values(value)) stack.push(child);
+ }
+ }
+ return false;
+}
+
+function sarifLevel(severity) {
+ return severity === "error" ? "error" : severity === "warning" ? "warning" : "note";
+}
+
+function relativeUri(file) {
+ const relative = path.isAbsolute(file) ? path.relative(process.cwd(), file) : file;
+ return (relative || path.basename(file)).split(path.sep).join("/");
+}
+
+function validateWorkflowShape(workflow) {
+ if (!workflow || typeof workflow !== "object" || Array.isArray(workflow) || !Array.isArray(workflow.nodes)) {
+ return "Expected an object with a nodes array";
+ }
+ if (workflow.nodes.length > MAX_NODES) return `Workflow exceeds the ${MAX_NODES} node safety limit`;
+ const names = new Set();
+ for (const node of workflow.nodes) {
+ if (!node || typeof node !== "object" || typeof node.name !== "string" || !node.name || typeof node.type !== "string" || !node.type) {
+ return "Every node must be an object with non-empty string name and type fields";
+ }
+ if (names.has(node.name)) return `Duplicate node name: ${safeDisplay(node.name)}`;
+ if (node.parameters !== undefined && (!node.parameters || typeof node.parameters !== "object" || Array.isArray(node.parameters))) {
+ return `Node parameters must be an object: ${safeDisplay(node.name)}`;
+ }
+ names.add(node.name);
+ }
+ if (workflow.connections !== undefined && (!workflow.connections || typeof workflow.connections !== "object" || Array.isArray(workflow.connections))) {
+ return "connections must be an object when present";
+ }
+ let edgeCount = 0;
+ for (const [source, outputs] of Object.entries(workflow.connections ?? {})) {
+ if (!names.has(source)) return `Connection source does not exist: ${safeDisplay(source)}`;
+ if (!outputs || typeof outputs !== "object" || Array.isArray(outputs)) {
+ return `Connection outputs must be an object: ${safeDisplay(source)}`;
+ }
+ for (const [outputType, outputGroups] of Object.entries(outputs)) {
+ if (outputType !== "main" && !/^ai_[a-z][a-z0-9_]*$/i.test(outputType)) {
+ return `Unsupported connection output type: ${safeDisplay(outputType)}`;
+ }
+ if (!Array.isArray(outputGroups)) return `Connection output ${safeDisplay(outputType)} must be an array`;
+ for (const connectionsAtIndex of outputGroups) {
+ if (connectionsAtIndex === null) continue;
+ if (!Array.isArray(connectionsAtIndex)) return `Connection index under ${safeDisplay(outputType)} must be an array`;
+ for (const connection of connectionsAtIndex) {
+ if (!connection || typeof connection !== "object" || Array.isArray(connection)
+ || typeof connection.node !== "string" || typeof connection.type !== "string"
+ || !Number.isInteger(connection.index) || connection.index < 0) {
+ return `Malformed connection from ${safeDisplay(source)}`;
+ }
+ if (connection.type !== outputType) return `Connection type does not match output ${safeDisplay(outputType)}`;
+ if (!names.has(connection.node)) return `Connection target does not exist: ${safeDisplay(connection.node)}`;
+ edgeCount += 1;
+ if (edgeCount > MAX_EDGES) return `Workflow exceeds the ${MAX_EDGES} edge safety limit`;
+ }
+ }
+ }
+ }
+ return null;
+}
+
+function severityRank(severity) {
+ return severity === "error" ? 2 : severity === "warning" ? 1 : 0;
+}
+
+function sameStringSet(left, right) {
+ const normalizedLeft = [...new Set(left.map((value) => value.toLowerCase()))].sort();
+ const normalizedRight = [...new Set(right.map((value) => value.toLowerCase()))].sort();
+ return normalizedLeft.length === normalizedRight.length
+ && normalizedLeft.every((value, index) => value === normalizedRight[index]);
+}
+
+function sortFindings(findings) {
+ return findings.sort((a, b) =>
+ a.ruleId.localeCompare(b.ruleId)
+ || String(a.node?.name ?? "").localeCompare(String(b.node?.name ?? ""))
+ || String(a.path ?? "").localeCompare(String(b.path ?? ""))
+ );
+}
+
+function finalizeFindings(findings, truncated) {
+ if (truncated && !findings.some((finding) => finding.ruleId === "VF000")) {
+ findings.push(invalidFinding(`Analysis stopped after ${MAX_FINDINGS_PER_FILE} findings`));
+ }
+ return sortFindings(findings);
+}
+
+export function safeDisplay(value) {
+ return String(value).replace(/[\u0000-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/g, " ");
+}
diff --git a/templates/final-report-template.md b/templates/final-report-template.md
deleted file mode 100644
index 3375553..0000000
--- a/templates/final-report-template.md
+++ /dev/null
@@ -1,35 +0,0 @@
-# Final Report Template
-
-## Summary
-
-[One short paragraph describing what the workflow does.]
-
-## What was created
-
-- [Trigger]
-- [Core processing node(s)]
-- [Branching logic]
-- [Destination node(s)]
-- [Error handling]
-
-## Assumptions used
-
-- [Assumption 1]
-- [Assumption 2]
-
-## Manual setup still required
-
-- [Credential setup]
-- [IDs / secrets / endpoint URLs]
-- [Field mapping review]
-
-## How to test
-
-1. [Step]
-2. [Step]
-3. [Step]
-
-## Suggested next upgrades
-
-- [Upgrade 1]
-- [Upgrade 2]
diff --git a/templates/intake-checklist.md b/templates/intake-checklist.md
deleted file mode 100644
index bc31665..0000000
--- a/templates/intake-checklist.md
+++ /dev/null
@@ -1,38 +0,0 @@
-# Intake Checklist
-
-Use this when interviewing the user.
-
-## Minimum information
-
-- Workflow goal
-- Trigger
-- Systems/apps involved
-- Final output/result
-
-## High-impact clarifiers
-
-- Frequency or schedule
-- Required fields or payload schema
-- Rules or exceptions
-- Approval steps
-- Error notifications
-- Create vs update behavior
-- Who owns credentials
-
-## Safe assumptions
-
-Usually okay to infer:
-
-- workflow name
-- basic formatting
-- standard retries
-- a generic error branch
-- common field names when obvious
-
-## Must confirm
-
-- deletions
-- payments/billing actions
-- external user messaging
-- compliance-sensitive handling
-- irreversible mutations
diff --git a/templates/system-prompt.md b/templates/system-prompt.md
deleted file mode 100644
index c243b43..0000000
--- a/templates/system-prompt.md
+++ /dev/null
@@ -1,131 +0,0 @@
-# System Prompt / Skill Prompt
-
-You are a workflow-building agent specialized in creating complete n8n workflows through MCP.
-
-Your role is to turn a user's natural-language automation request into a usable workflow inside n8n while keeping the process simple, practical, and transparent.
-
-## Mission
-
-Help the user move from idea to working automation with minimal friction.
-
-You should:
-- understand the workflow goal,
-- ask only the minimum essential follow-up questions,
-- create a normalized plan before building,
-- create or update the workflow through n8n MCP tools,
-- validate the result,
-- return a clear final handoff report.
-
-## Tone
-
-Be:
-- practical
-- concise
-- collaborative
-- intuitive
-- calm
-
-Do not over-interview the user.
-Do not flood the conversation with jargon.
-
-## Mandatory process
-
-### 1. Understand the intent
-Identify:
-- trigger
-- systems involved
-- desired final outcome
-- key business rules
-- important exceptions
-
-### 2. Ask only high-value questions
-Ask follow-ups only when the answer changes the architecture, creates operational risk, or affects external communication.
-
-### 3. Produce a normalized plan before building
-Always create a normalized plan with:
-- workflow_name
-- workflow_mode
-- business_goal
-- trigger
-- systems_involved
-- key_steps
-- branching_logic
-- credentials_required
-- error_handling
-- assumptions
-- open_questions
-- test_strategy
-
-### 4. Build through n8n MCP
-Create or update the workflow.
-Use readable node names.
-Prefer understandable graphs over clever complexity.
-Leave placeholders where secrets, IDs, or credentials are missing.
-
-### 5. Validate
-Check for:
-- disconnected nodes,
-- missing required fields,
-- absent credentials,
-- unsupported assumptions,
-- weak or missing failure paths,
-- undefined terminal behavior in branches.
-
-### 6. Deliver a handoff report
-The final report should include:
-- what was created,
-- assumptions used,
-- missing manual setup,
-- how to test,
-- suggested next upgrades.
-
-## Defaults policy
-
-You may assume low-risk defaults for:
-- workflow naming,
-- standard retries,
-- formatting details,
-- common internal notifications,
-- simple field mapping.
-
-You must ask before assuming:
-- destructive actions,
-- financial side effects,
-- customer-facing communications,
-- legal or compliance-sensitive rules,
-- approval policies,
-- create-only vs upsert when duplicates matter.
-
-## Mode policy
-
-### fast
-Use fewer questions and more defaults.
-Best for prototypes.
-
-### balanced
-Default mode.
-Use limited follow-ups with practical safeguards.
-
-### safe
-Use more explicit confirmations, stronger validation, and fewer silent assumptions.
-
-## Lightweight opening pattern
-
-When a request is underspecified, start with these questions:
-1. What should trigger the workflow?
-2. Which apps or systems are involved?
-3. What should happen from start to finish?
-4. Are there any rules, approvals, or exceptions I should respect?
-
-## Planning-first rule
-
-Never jump straight into building unless the workflow is already sufficiently specified.
-
-## Delivery quality bar
-
-A good result is:
-- usable,
-- understandable,
-- explicit about assumptions,
-- honest about missing credentials or manual setup,
-- easy for the user to continue from.
diff --git a/test/fixtures/invalid.workflow.json b/test/fixtures/invalid.workflow.json
new file mode 100644
index 0000000..531d1ce
--- /dev/null
+++ b/test/fixtures/invalid.workflow.json
@@ -0,0 +1,3 @@
+{
+ "name": "Not an n8n workflow"
+}
diff --git a/test/vibeflow.test.mjs b/test/vibeflow.test.mjs
new file mode 100644
index 0000000..6cac8b5
--- /dev/null
+++ b/test/vibeflow.test.mjs
@@ -0,0 +1,467 @@
+import assert from "node:assert/strict";
+import { spawnSync } from "node:child_process";
+import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
+import { tmpdir } from "node:os";
+import path from "node:path";
+import test from "node:test";
+import { fileURLToPath } from "node:url";
+
+import {
+ MAX_FINDINGS_PER_FILE,
+ MAX_NODES,
+ checkFile,
+ checkPaths,
+ formatText,
+ formatSarif,
+ inspectWorkflow,
+ normalizeConfig,
+} from "../src/vibeflow.mjs";
+
+const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
+const safe = path.join(root, "examples/safe-support-agent.workflow.json");
+const unsafe = path.join(root, "examples/unsafe-support-agent.workflow.json");
+const invalid = path.join(root, "test/fixtures/invalid.workflow.json");
+
+test("safe workflow passes all default policies", async () => {
+ const result = await checkFile(safe);
+ assert.deepEqual(result.findings, []);
+});
+
+test("unsafe workflow exercises every v0.8 policy", async () => {
+ const result = await checkFile(unsafe);
+ assert.deepEqual(
+ [...new Set(result.findings.map((finding) => finding.ruleId))],
+ ["VF001", "VF002", "VF003", "VF004", "VF005", "VF006", "VF007", "VF008", "VF009"],
+ );
+});
+
+test("invalid workflow produces VF000", async () => {
+ const result = await checkFile(invalid);
+ assert.equal(result.findings.length, 1);
+ assert.equal(result.findings[0].ruleId, "VF000");
+});
+
+test("rule severity and vocabulary are configurable", async () => {
+ const workflow = JSON.parse(await readFile(unsafe, "utf8"));
+ workflow.nodes.unshift({
+ id: "custom-guard",
+ name: "Emergency brake",
+ type: "n8n-nodes-base.if",
+ parameters: { conditions: { boolean: [{ value1: "={{ $json.emergency_brake }}", value2: true }] } },
+ });
+ workflow.connections["Public Webhook"] = {
+ main: [[{ node: "Emergency brake", type: "main", index: 0 }]],
+ };
+ workflow.connections["Emergency brake"] = {
+ main: [[{ node: "AI Agent", type: "main", index: 0 }], []],
+ };
+
+ const config = normalizeConfig({
+ rules: { VF003: "off" },
+ terms: { killSwitch: ["emergency brake"] },
+ });
+ const findings = inspectWorkflow(workflow, config);
+ assert.equal(findings.some((finding) => finding.ruleId === "VF003"), false);
+ assert.equal(findings.some((finding) => finding.ruleId === "VF006"), false);
+});
+
+test("SARIF report contains rules and file locations", async () => {
+ const report = await checkPaths([unsafe]);
+ const sarif = JSON.parse(formatSarif(report));
+ assert.equal(sarif.version, "2.1.0");
+ assert.equal(sarif.runs[0].tool.driver.name, "Vibeflow");
+ assert.ok(sarif.runs[0].results.length >= 8);
+ assert.equal(sarif.runs[0].results[0].locations[0].physicalLocation.region.startLine, 1);
+});
+
+test("CLI exits 1 on errors and supports non-blocking automation", () => {
+ const blocking = runCli(["check", unsafe, "--format", "json"]);
+ assert.equal(blocking.status, 1, blocking.stderr);
+ assert.equal(JSON.parse(blocking.stdout).summary.errors > 0, true);
+
+ const advisory = runCli(["check", unsafe, "--fail-on", "never"]);
+ assert.equal(advisory.status, 0, advisory.stderr);
+});
+
+test("plugin and skill manifests contain no scaffold placeholders", async () => {
+ const pluginPath = path.join(root, "plugins/vibeflow/.codex-plugin/plugin.json");
+ const skillPath = path.join(root, "plugins/vibeflow/skills/vibeflow/SKILL.md");
+ const plugin = JSON.parse(await readFile(pluginPath, "utf8"));
+ const skill = await readFile(skillPath, "utf8");
+ assert.equal(plugin.name, "vibeflow");
+ assert.equal(plugin.version, "0.8.0");
+ assert.match(skill, /^---\nname: vibeflow\ndescription: .+\n---/);
+ assert.doesNotMatch(`${JSON.stringify(plugin)}\n${skill}`, /\[TODO:/);
+});
+
+test("unknown policy configuration fails closed", () => {
+ assert.throws(() => normalizeConfig({ rules: { VF999: "off" } }), /Unknown configurable rule/);
+ assert.throws(() => normalizeConfig({ surprise: true }), /Unknown config key/);
+});
+
+test("locked policy rejects configuration weakening", () => {
+ assert.throws(() => normalizeConfig({ rules: { VF001: "off" } }, { locked: true }), /cannot weaken VF001/);
+ assert.throws(() => normalizeConfig({ terms: { killSwitch: ["anything"] } }, { locked: true }), /cannot change/);
+ assert.throws(() => normalizeConfig({ bannedNodeTypes: [] }, { locked: true }), /cannot remove/);
+ assert.equal(normalizeConfig({ rules: { VF003: "error" } }, { locked: true }).rules.VF003, "error");
+});
+
+test("webhook authentication requires a supported mode and credential reference", () => {
+ const webhook = { name: "Webhook", type: "n8n-nodes-base.webhook", parameters: { authentication: "headerAuth" } };
+ assert.equal(hasRule(workflowWith([webhook]), "VF003"), true);
+ webhook.credentials = { httpHeaderAuth: { id: "credential-reference", name: "Header Auth" } };
+ assert.equal(hasRule(workflowWith([webhook]), "VF003"), false);
+ webhook.credentials = { httpHeaderAuth: {} };
+ assert.equal(hasRule(workflowWith([webhook]), "VF003"), true);
+ webhook.credentials = { unrelatedCredential: { id: "credential-reference" } };
+ assert.equal(hasRule(workflowWith([webhook]), "VF003"), true);
+ webhook.parameters.authentication = "madeUpAuth";
+ assert.equal(hasRule(workflowWith([webhook]), "VF003"), true);
+});
+
+test("raw headers and URL query credentials trigger VF001", () => {
+ const workflow = workflowWith([{
+ name: "Payload",
+ type: "n8n-nodes-base.noOp",
+ parameters: {
+ jsonHeaders: "{\"Authorization\":\"Bearer live_example_0123456789abcdef\"}",
+ url: "https://api.invalid/v1?access_token=live_0123456789abcdef",
+ mixed: "Bearer literal_0123456789abcdef {{ $json.id }}",
+ expressionLiteral: "={{ 'Bearer live_secret_0123456789abcdef' }}",
+ },
+ }]);
+ const secrets = inspectWorkflow(workflow).filter((finding) => finding.ruleId === "VF001");
+ assert.equal(secrets.length, 4);
+});
+
+test("pinned and static workflow data are scanned for secrets", () => {
+ const workflow = workflowWith([{ name: "Manual", type: "n8n-nodes-base.manualTrigger", parameters: {} }]);
+ workflow.pinData = { Manual: [{ json: { access_token: "live_0123456789abcdef" } }] };
+ workflow.staticData = { password: "literal_password_0123456789" };
+ const paths = inspectWorkflow(workflow).filter((finding) => finding.ruleId === "VF001").map((finding) => finding.path);
+ assert.deepEqual(paths, ["pinData.Manual[0].json.access_token", "staticData.password"]);
+});
+
+test("idempotency must be atomic and dominate inbound side-effect paths", () => {
+ const nodes = [
+ { name: "Webhook", type: "n8n-nodes-base.webhook", parameters: { authentication: "none" } },
+ { name: "Send", type: "n8n-nodes-base.httpRequest", parameters: {} },
+ { name: "Idempotency note", type: "n8n-nodes-base.noOp", parameters: {} },
+ ];
+ const disconnected = workflowWith(nodes, {
+ Webhook: { main: [[connection("Send")]] },
+ });
+ assert.equal(findingFor(disconnected, "VF005", "Send"), true);
+
+ const guard = {
+ name: "Claim idempotency event",
+ type: "n8n-nodes-base.postgres",
+ parameters: { query: "INSERT INTO event_ledger VALUES ($1) ON CONFLICT DO NOTHING RETURNING event_id" },
+ };
+ const guarded = workflowWith([nodes[0], guard, nodes[1]], {
+ Webhook: { main: [[connection(guard.name)]] },
+ [guard.name]: { main: [[connection("Send")]] },
+ });
+ assert.equal(findingFor(guarded, "VF005", "Send"), false);
+
+ guard.parameters.query = "SELECT 'event id' AS label, 'on conflict' AS note";
+ const superficial = workflowWith([nodes[0], guard, nodes[1]], {
+ Webhook: { main: [[connection(guard.name)]] },
+ [guard.name]: { main: [[connection("Send")]] },
+ });
+ assert.equal(findingFor(superficial, "VF005", "Send"), true);
+
+ guard.parameters.query = "INSERT INTO audit(message) VALUES ('idempotency'); -- ON CONFLICT (event_id) DO NOTHING";
+ assert.equal(findingFor(workflowWith([nodes[0], guard, nodes[1]], {
+ Webhook: { main: [[connection(guard.name)]] },
+ [guard.name]: { main: [[connection("Send")]] },
+ }), "VF005", "Send"), true);
+});
+
+test("kill switches must be structural and dominate every AI entry path", () => {
+ const webhook = { name: "Webhook", type: "n8n-nodes-base.webhook", parameters: { authentication: "none" } };
+ const guard = {
+ name: "Agent enabled kill switch",
+ type: "n8n-nodes-base.if",
+ parameters: { conditions: { boolean: [{ value1: "={{ $json.agent_enabled }}", value2: true }] } },
+ };
+ const ai = { name: "AI Agent", type: "@n8n/n8n-nodes-langchain.agent", parameters: {} };
+ const bypassed = workflowWith([webhook, guard, ai], {
+ Webhook: { main: [[connection(guard.name), connection(ai.name)]] },
+ [guard.name]: { main: [[connection(ai.name)]] },
+ });
+ assert.equal(findingFor(bypassed, "VF006", ai.name), true);
+
+ const namedNoOp = { ...guard, name: "Agent enabled check", type: "n8n-nodes-base.noOp" };
+ const superficial = workflowWith([webhook, namedNoOp, ai], {
+ Webhook: { main: [[connection(namedNoOp.name)]] },
+ [namedNoOp.name]: { main: [[connection(ai.name)]] },
+ });
+ assert.equal(findingFor(superficial, "VF006", ai.name), true);
+
+ const constantGuard = {
+ ...guard,
+ parameters: { conditions: { boolean: [{ value1: true, value2: true }], note: "agent enabled" } },
+ };
+ const constant = workflowWith([webhook, constantGuard, ai], {
+ Webhook: { main: [[connection(constantGuard.name)]] },
+ [constantGuard.name]: { main: [[connection(ai.name)]] },
+ });
+ assert.equal(findingFor(constant, "VF006", ai.name), true);
+
+ const invertedGuard = {
+ ...guard,
+ parameters: { conditions: { boolean: [{ value1: "={{ $json.agent_enabled }}", value2: false }] } },
+ };
+ const inverted = workflowWith([webhook, invertedGuard, ai], {
+ Webhook: { main: [[connection(invertedGuard.name)]] },
+ [invertedGuard.name]: { main: [[connection(ai.name)], []] },
+ });
+ assert.equal(findingFor(inverted, "VF006", ai.name), true);
+
+ const codeComment = {
+ name: "Agent enabled kill switch",
+ type: "n8n-nodes-base.code",
+ parameters: { jsCode: "// {{ $json.agent_enabled }}\nreturn items;" },
+ };
+ const decorativeCode = workflowWith([webhook, codeComment, ai], {
+ Webhook: { main: [[connection(codeComment.name)]] },
+ [codeComment.name]: { main: [[connection(ai.name)], []] },
+ });
+ assert.equal(findingFor(decorativeCode, "VF006", ai.name), true);
+
+ const guarded = workflowWith([webhook, guard, ai], {
+ Webhook: { main: [[connection(guard.name)]] },
+ [guard.name]: { main: [[connection(ai.name)], []] },
+ });
+ assert.equal(findingFor(guarded, "VF006", ai.name), false);
+});
+
+test("human handoff requires a reachable external action", () => {
+ const ai = { name: "AI Agent", type: "@n8n/n8n-nodes-langchain.agent", parameters: {} };
+ const fake = { name: "Human handoff ticket", type: "n8n-nodes-base.noOp", parameters: {} };
+ const superficial = workflowWith([ai, fake], { [ai.name]: { main: [[connection(fake.name)]] } });
+ assert.equal(findingFor(superficial, "VF007", ai.name), true);
+
+ const slack = { ...fake, type: "n8n-nodes-base.slack" };
+ const real = workflowWith([ai, slack], { [ai.name]: { main: [[connection(slack.name)]] } });
+ assert.equal(findingFor(real, "VF007", ai.name), false);
+
+ const getTicket = {
+ name: "Human handoff ticket",
+ type: "n8n-nodes-base.httpRequest",
+ parameters: { method: "GET", url: "https://support.invalid/tickets" },
+ };
+ const readOnly = workflowWith([ai, getTicket], { [ai.name]: { main: [[connection(getTicket.name)]] } });
+ assert.equal(findingFor(readOnly, "VF007", ai.name), true);
+});
+
+test("common mutable SaaS nodes receive side-effect policies", () => {
+ const webhook = {
+ name: "Webhook",
+ type: "n8n-nodes-base.webhook",
+ parameters: { authentication: "headerAuth" },
+ credentials: { httpHeaderAuth: { id: "credential-reference" } },
+ };
+ const airtable = {
+ name: "Create Airtable record",
+ type: "n8n-nodes-base.airtable",
+ retryOnFail: true,
+ maxTries: 99,
+ waitBetweenTries: 0,
+ parameters: { operation: "create" },
+ };
+ const workflow = workflowWith([webhook, airtable], {
+ Webhook: { main: [[connection(airtable.name)]] },
+ }, {});
+ const rules = new Set(inspectWorkflow(workflow).map((finding) => finding.ruleId));
+ for (const rule of ["VF004", "VF005", "VF008", "VF009"]) assert.equal(rules.has(rule), true, rule);
+});
+
+test("error handling must be connected or configured at workflow level", () => {
+ const request = { name: "Request", type: "n8n-nodes-base.httpRequest", continueOnFail: true, parameters: {} };
+ const disconnectedError = { name: "Error", type: "n8n-nodes-base.errorTrigger", parameters: {} };
+ assert.equal(findingFor(workflowWith([request, disconnectedError]), "VF004", request.name), true);
+
+ const stop = { name: "Failure stop", type: "n8n-nodes-base.noOp", parameters: {} };
+ request.onError = "continueErrorOutput";
+ const handled = workflowWith([request, stop], {
+ Request: { main: [[], [connection(stop.name)]] },
+ });
+ assert.equal(findingFor(handled, "VF004", request.name), false);
+
+ const selfLoop = workflowWith([request], {
+ Request: { main: [[], [connection(request.name)]] },
+ });
+ assert.equal(findingFor(selfLoop, "VF004", request.name), true);
+
+ const loop = { name: "Loop", type: "n8n-nodes-base.noOp", parameters: {} };
+ const indirectLoop = workflowWith([request, loop], {
+ Request: { main: [[], [connection(loop.name)]] },
+ Loop: { main: [[connection(request.name)]] },
+ });
+ assert.equal(findingFor(indirectLoop, "VF004", request.name), true);
+});
+
+test("malformed connection metadata cannot fabricate graph edges", () => {
+ const workflow = workflowWith([
+ { name: "AI Agent", type: "@n8n/n8n-nodes-langchain.agent", parameters: {} },
+ { name: "Human handoff", type: "n8n-nodes-base.slack", parameters: {} },
+ ], {
+ "AI Agent": { metadata: [[{ node: "Human handoff", type: "metadata", index: 0 }]] },
+ });
+ assert.equal(inspectWorkflow(workflow)[0].ruleId, "VF000");
+});
+
+test("timeouts have a finite upper bound", () => {
+ const workflow = workflowWith([
+ { name: "Request", type: "n8n-nodes-base.httpRequest", parameters: {} },
+ ], {}, { executionTimeout: 1e300 });
+ assert.equal(hasRule(workflow, "VF008"), true);
+ workflow.settings.executionTimeout = true;
+ assert.equal(hasRule(workflow, "VF008"), true);
+});
+
+test("AI resource connections do not create control-flow bypasses", () => {
+ const trigger = { name: "Webhook", type: "n8n-nodes-base.webhook", parameters: { authentication: "none" } };
+ const guard = {
+ name: "Agent enabled kill switch",
+ type: "n8n-nodes-base.if",
+ parameters: { conditions: { boolean: [{ value1: "={{ $json.agent_enabled }}", value2: true }] } },
+ };
+ const ai = { name: "AI Agent", type: "@n8n/n8n-nodes-langchain.agent", parameters: {} };
+ const model = { name: "Chat Model", type: "@n8n/n8n-nodes-langchain.lmChatOpenAi", parameters: {} };
+ const workflow = workflowWith([trigger, guard, ai, model], {
+ Webhook: { main: [[connection(guard.name)]] },
+ [guard.name]: { main: [[connection(ai.name)], []] },
+ [model.name]: { ai_languageModel: [[{ node: ai.name, type: "ai_languageModel", index: 0 }]] },
+ });
+ assert.equal(findingFor(workflow, "VF006", ai.name), false);
+});
+
+test("analysis budgets cap nodes and findings", () => {
+ const tooManyNodes = Array.from({ length: MAX_NODES + 1 }, (_, index) => ({
+ name: `Node ${index}`,
+ type: "n8n-nodes-base.noOp",
+ parameters: {},
+ }));
+ assert.equal(inspectWorkflow(workflowWith(tooManyNodes))[0].ruleId, "VF000");
+
+ const parameters = Object.fromEntries(Array.from({ length: MAX_FINDINGS_PER_FILE + 100 }, (_, index) => [
+ `token_${index}`,
+ `literal-secret-${index.toString().padStart(8, "0")}`,
+ ]));
+ const findings = inspectWorkflow(workflowWith([{ name: "Secrets", type: "n8n-nodes-base.noOp", parameters }]));
+ assert.equal(findings.length, MAX_FINDINGS_PER_FILE + 1);
+ assert.equal(findings.some((finding) => finding.ruleId === "VF000"), true);
+});
+
+test("late-stage finding truncation always fails closed", () => {
+ const webhook = {
+ name: "Webhook",
+ type: "n8n-nodes-base.webhook",
+ parameters: { authentication: "headerAuth" },
+ credentials: { httpHeaderAuth: { id: "credential-reference" } },
+ };
+ const ai = { name: "AI Agent", type: "@n8n/n8n-nodes-langchain.agent", parameters: {} };
+ const sideEffects = Array.from({ length: MAX_FINDINGS_PER_FILE + 1 }, (_, index) => ({
+ name: `Request ${index}`,
+ type: "n8n-nodes-base.httpRequest",
+ parameters: {},
+ }));
+ const connections = {
+ Webhook: { main: [[...sideEffects.map((node) => connection(node.name)), connection(ai.name)]] },
+ };
+ const findings = inspectWorkflow(workflowWith([webhook, ai, ...sideEffects], connections, {
+ executionTimeout: 120,
+ errorWorkflow: "error-handler-id",
+ }));
+ assert.equal(findings.some((finding) => finding.ruleId === "VF000"), true);
+ assert.equal(findings.some((finding) => finding.severity === "error"), true);
+});
+
+test("duplicate node names fail workflow validation", async (t) => {
+ const directory = await mkdtemp(path.join(tmpdir(), "vibeflow-test-"));
+ t.after(() => rm(directory, { recursive: true, force: true }));
+ const file = path.join(directory, "duplicate.workflow.json");
+ await writeFile(file, JSON.stringify({
+ nodes: [
+ { name: "Duplicate", type: "n8n-nodes-base.manualTrigger", parameters: {} },
+ { name: "Duplicate", type: "n8n-nodes-base.noOp", parameters: {} },
+ ],
+ connections: {},
+ }));
+
+ const result = await checkFile(file);
+ assert.equal(result.findings[0].ruleId, "VF000");
+ assert.match(result.findings[0].message, /Duplicate node name/);
+});
+
+test("text output strips terminal control characters", () => {
+ const report = {
+ tool: { name: "vibeflow", version: "0.8.0" },
+ files: [{
+ file: "bad\u001b[2J.workflow.json",
+ workflowName: "hostile\nname",
+ findings: [{
+ ruleId: "VF000",
+ severity: "error",
+ message: "bad\rmessage",
+ node: { name: "node\u001b[31m" },
+ }],
+ }],
+ summary: { files: 1, errors: 1, warnings: 0 },
+ };
+
+ const output = formatText(report);
+ assert.doesNotMatch(output, /\u001b|\r/);
+ assert.doesNotMatch(output, /hostile\nname/);
+});
+
+test("CLI sanitizes control characters in error output", async (t) => {
+ const directory = await mkdtemp(path.join(tmpdir(), "vibeflow-config-"));
+ t.after(() => rm(directory, { recursive: true, force: true }));
+ const config = path.join(directory, "hostile.json");
+ await writeFile(config, JSON.stringify({ "\u001b[2J\u202e": true }));
+
+ const result = runCli(["check", safe, "--config", config]);
+ assert.equal(result.status, 2);
+ assert.doesNotMatch(result.stderr, /\u001b|\u202e/);
+ assert.match(result.stderr, /Unknown config key/);
+});
+
+test("deeply nested AI parameters do not overflow the call stack", () => {
+ let parameters = { value: "safe" };
+ for (let index = 0; index < 15_000; index += 1) parameters = { nested: parameters };
+ const findings = inspectWorkflow({
+ name: "Deep workflow",
+ nodes: [{ name: "AI Agent", type: "@n8n/n8n-nodes-langchain.agent", parameters }],
+ connections: {},
+ settings: { executionTimeout: 120 },
+ });
+
+ assert.equal(findings.some((finding) => finding.ruleId === "VF006"), true);
+});
+
+function workflowWith(nodes, connections = {}, settings = { executionTimeout: 120 }) {
+ return { name: "Test workflow", nodes, connections, settings };
+}
+
+function connection(node) {
+ return { node, type: "main", index: 0 };
+}
+
+function hasRule(workflow, ruleId) {
+ return inspectWorkflow(workflow).some((finding) => finding.ruleId === ruleId);
+}
+
+function findingFor(workflow, ruleId, nodeName) {
+ return inspectWorkflow(workflow).some((finding) => finding.ruleId === ruleId && finding.node?.name === nodeName);
+}
+
+function runCli(args) {
+ return spawnSync(process.execPath, [path.join(root, "bin/vibeflow.mjs"), ...args], {
+ cwd: root,
+ encoding: "utf8",
+ });
+}
From f6d8e670ba5bc59ac4251c65264e0aa1825b9cc3 Mon Sep 17 00:00:00 2001
From: Felipe Domingues <53182096+domfelipe@users.noreply.github.com>
Date: Wed, 22 Jul 2026 11:47:01 -0300
Subject: [PATCH 2/9] Pin Vibeflow release references
---
README.md | 8 ++++----
docs/release-audit.md | 2 +-
plugins/vibeflow/skills/vibeflow/SKILL.md | 2 +-
3 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/README.md b/README.md
index d2af04a..5b85a6a 100644
--- a/README.md
+++ b/README.md
@@ -14,7 +14,7 @@ Vibeflow is not another workflow builder or MCP server. It is a deterministic qu
## Quick start
```bash
-npx --yes github:domfelipe/vibeflow-n8n#v0.8.0 check workflow.json
+npx --yes github:domfelipe/vibeflow-n8n#4998605ed7dc12b9b867d69d7005d25778c7e109 check workflow.json
```
Or from a checkout:
@@ -73,8 +73,8 @@ Directories are searched recursively for `*.workflow.json` files.
### GitHub Action
```yaml
-- uses: actions/checkout@v4
-- uses: domfelipe/vibeflow-n8n@v0.8.0
+- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
+- uses: domfelipe/vibeflow-n8n@4998605ed7dc12b9b867d69d7005d25778c7e109 # v0.8.0 code
with:
path: workflows/
output: vibeflow.sarif
@@ -83,7 +83,7 @@ Directories are searched recursively for `*.workflow.json` files.
## Codex plugin
```bash
-codex plugin marketplace add domfelipe/vibeflow-n8n
+codex plugin marketplace add domfelipe/vibeflow-n8n --ref 4998605ed7dc12b9b867d69d7005d25778c7e109
```
Install **Vibeflow** from the Plugins Directory, then ask:
diff --git a/docs/release-audit.md b/docs/release-audit.md
index c5b3b81..6dc7d23 100644
--- a/docs/release-audit.md
+++ b/docs/release-audit.md
@@ -41,7 +41,7 @@ At the 5,000-node limit, the corrected linear traversal completed the synthetic
- The package has no runtime dependencies and uses a publish allowlist.
- The bundled action always enables `--locked`.
-The first merge SHA must replace mutable self-references in the README and Codex skill before the release tag is created.
+The first merge SHA, `4998605ed7dc12b9b867d69d7005d25778c7e109`, pins the CLI, GitHub Action, and Codex marketplace examples before the release tag is created.
## Residual limitations
diff --git a/plugins/vibeflow/skills/vibeflow/SKILL.md b/plugins/vibeflow/skills/vibeflow/SKILL.md
index aedc652..16d1ca1 100644
--- a/plugins/vibeflow/skills/vibeflow/SKILL.md
+++ b/plugins/vibeflow/skills/vibeflow/SKILL.md
@@ -19,7 +19,7 @@ Use the CLI as the source of truth. Do not infer that a workflow is safe from it
For a released version without a checkout:
```bash
- npx --yes github:domfelipe/vibeflow-n8n#v0.8.0 check path/to/workflow.json
+ npx --yes github:domfelipe/vibeflow-n8n#4998605ed7dc12b9b867d69d7005d25778c7e109 check path/to/workflow.json
```
3. Read [references/policies.md](references/policies.md) when interpreting or repairing a finding.
From 5d2db1de87c301480bdab5d7ce6f68ad07b575f1 Mon Sep 17 00:00:00 2001
From: Felipe Domingues <53182096+domfelipe@users.noreply.github.com>
Date: Wed, 22 Jul 2026 11:51:33 -0300
Subject: [PATCH 3/9] Record Vibeflow launch evidence
---
docs/codex-for-oss-application.md | 24 ++++++++++++------------
docs/launch.md | 18 ++++++++++++------
2 files changed, 24 insertions(+), 18 deletions(-)
diff --git a/docs/codex-for-oss-application.md b/docs/codex-for-oss-application.md
index d2c3211..84e0cd3 100644
--- a/docs/codex-for-oss-application.md
+++ b/docs/codex-for-oss-application.md
@@ -6,7 +6,7 @@ Application:
Vibeflow is public, MIT-licensed, owned by its principal maintainer, and aligned with Codex maintainer workflows. Version 0.8.0 establishes active engineering evidence but does not manufacture adoption.
-Do not submit until the live evidence section is refreshed and contains external usage.
+**Current decision: do not submit.** The release is live, but the external-usage gate is not yet met.
## Practical submission gate
@@ -18,17 +18,17 @@ These are internal quality targets, not official OpenAI thresholds:
- current traffic, clone, installation, or dependent-project evidence;
- no confidential information in the application.
-## Live evidence
+## Live evidence snapshot
-Refresh immediately before submission:
+Captured from GitHub on 2026-07-22 immediately after release:
-- GitHub stars: `[refresh]`
-- forks: `[refresh]`
-- unique clones in the latest available period: `[refresh]`
-- releases and latest release date: `[refresh]`
-- external contributors: `[refresh]`
-- external users or public references: `[refresh]`
-- maintainer examples: `[refresh issues, reviews, and releases]`
+- GitHub stars: 1; ownership is not attributed, so it is not counted as external adoption.
+- forks: 0.
+- traffic: 2 unique clones in the available 14-day window, both before release and not attributable to external users.
+- releases: 1; `v0.8.0` published on 2026-07-22.
+- external contributors: 0.
+- external users or independent public references: 0 verified.
+- maintainer evidence: [PR #1](https://github.com/domfelipe/vibeflow-n8n/pull/1), [PR #2](https://github.com/domfelipe/vibeflow-n8n/pull/2), [release v0.8.0](https://github.com/domfelipe/vibeflow-n8n/releases/tag/v0.8.0), and [launch discussion #3](https://github.com/domfelipe/vibeflow-n8n/discussions/3).
## Form draft
@@ -38,7 +38,7 @@ Principal maintainer.
### Why is this repository eligible?
-> Vibeflow is an MIT-licensed safety gate for AI-generated n8n workflows. It catches embedded secrets, unsafe webhooks, missing kill switches, human handoffs, idempotency, error paths, timeouts, and risky retries before deployment. It is maintained as a dependency-free CLI, GitHub Action, and Codex plugin. [Add refreshed external usage evidence before submitting.]
+> Vibeflow is an MIT-licensed safety gate for AI-generated n8n workflows. It catches embedded secrets, unsafe webhooks, missing kill switches, human handoffs, idempotency, error paths, timeouts, and risky retries before deployment. It is maintained as a dependency-free CLI, GitHub Action, and Codex plugin. External usage evidence is not yet sufficient, so submission is intentionally deferred.
### How will API credits be used?
@@ -50,4 +50,4 @@ Principal maintainer.
## Final verification
-Before submitting, confirm the GitHub profile and repository are public, replace every `[refresh]` marker, verify each form answer remains under 500 characters, and use accurate current evidence only.
+Before submitting, capture a fresh evidence snapshot, confirm the GitHub profile and repository remain public, verify each form answer remains under 500 characters, and use accurate current evidence only. Replace the deferral sentence only after the practical submission gate is met.
diff --git a/docs/launch.md b/docs/launch.md
index 17a7423..2c24a99 100644
--- a/docs/launch.md
+++ b/docs/launch.md
@@ -2,12 +2,18 @@
## Release gate
-- `npm run verify` passes on Node.js 20, 22, and 24.
-- Official skill and plugin validators pass.
-- Safe fixture exits 0; unsafe fixture exits 1.
-- SARIF is valid JSON and uploaded by CI.
-- Repository description and topics match the new product.
-- `v0.8.0` release notes match `CHANGELOG.md`.
+- [x] `npm run verify` passes on Node.js 20, 22, and 24 in CI.
+- [x] Official skill and plugin validators pass.
+- [x] Safe fixture exits 0; unsafe fixture exits 1.
+- [x] SARIF is valid JSON and uploaded by CI.
+- [x] Repository description and topics match the new product.
+- [x] `v0.8.0` release notes match `CHANGELOG.md`.
+- [x] Remote CLI and pinned Codex marketplace install successfully.
+- [x] Private vulnerability reporting and Discussions are enabled.
+
+Release:
+
+Launch discussion:
## Announcement
From e738a5e070ef877f687d9a67a6a9bd0a1105e052 Mon Sep 17 00:00:00 2001
From: Felipe Domingues <53182096+domfelipe@users.noreply.github.com>
Date: Wed, 22 Jul 2026 13:18:18 -0300
Subject: [PATCH 4/9] Prepare Codex for OSS submission pack
---
docs/codex-for-oss-application.md | 158 ++++++++++++++++++++++++------
1 file changed, 129 insertions(+), 29 deletions(-)
diff --git a/docs/codex-for-oss-application.md b/docs/codex-for-oss-application.md
index 84e0cd3..ba062db 100644
--- a/docs/codex-for-oss-application.md
+++ b/docs/codex-for-oss-application.md
@@ -1,53 +1,153 @@
-# Codex for Open Source application gate
+# Codex for Open Source β submission pack
Application:
-## Current position
+Program overview:
-Vibeflow is public, MIT-licensed, owned by its principal maintainer, and aligned with Codex maintainer workflows. Version 0.8.0 establishes active engineering evidence but does not manufacture adoption.
+Program terms:
-**Current decision: do not submit.** The release is live, but the external-usage gate is not yet met.
+Prepared: 2026-07-22
-## Practical submission gate
+## Recommendation
-These are internal quality targets, not official OpenAI thresholds:
+Submit in **English**, even through the PT-BR form. OpenAI publishes no language requirement and no evidence that language changes selection odds. English is recommended only to reduce translation friction for a global technical review.
-- a tagged public release with green CI;
-- at least three unrelated external users or teams with verifiable feedback;
-- at least one external issue, discussion, or pull request with maintainer activity;
-- current traffic, clone, installation, or dependent-project evidence;
-- no confidential information in the application.
+Submit now. Stars, forks, downloads, repository usage, ecosystem importance, and active maintenance are evaluation signals, not published minimum thresholds. Vibeflow's strongest evidence is its Codex-native engineering and its first real 92-node audit, not early popularity metrics.
-## Live evidence snapshot
+## Copy-and-paste form
-Captured from GitHub on 2026-07-22 immediately after release:
+### First name
-- GitHub stars: 1; ownership is not attributed, so it is not counted as external adoption.
-- forks: 0.
-- traffic: 2 unique clones in the available 14-day window, both before release and not attributable to external users.
-- releases: 1; `v0.8.0` published on 2026-07-22.
-- external contributors: 0.
-- external users or independent public references: 0 verified.
-- maintainer evidence: [PR #1](https://github.com/domfelipe/vibeflow-n8n/pull/1), [PR #2](https://github.com/domfelipe/vibeflow-n8n/pull/2), [release v0.8.0](https://github.com/domfelipe/vibeflow-n8n/releases/tag/v0.8.0), and [launch discussion #3](https://github.com/domfelipe/vibeflow-n8n/discussions/3).
+`Felipe`
-## Form draft
+### Last name
-### Role
+`Domingues`
-Principal maintainer.
+### Email
+
+`[FILL PRIVATELY: email associated with the ChatGPT account]`
+
+### GitHub username
+
+`domfelipe`
+
+### GitHub repository URL
+
+`https://github.com/domfelipe/vibeflow-n8n`
+
+### Maintainer role
+
+Select: **Principal maintainer**.
### Why is this repository eligible?
-> Vibeflow is an MIT-licensed safety gate for AI-generated n8n workflows. It catches embedded secrets, unsafe webhooks, missing kill switches, human handoffs, idempotency, error paths, timeouts, and risky retries before deployment. It is maintained as a dependency-free CLI, GitHub Action, and Codex plugin. External usage evidence is not yet sufficient, so submission is intentionally deferred.
+Character count: **420/500**.
+
+> Vibeflow is an MIT-licensed safety and contract gate for AI-generated n8n workflows, rebuilt end-to-end with Codex. Its v0.8.0 plugin audited a real 92-node workflow in read-only mode and produced provenance-backed results: 3 blocking AI-safety findings, 57 prioritized warnings, and concrete revalidation gates. It fills a growing n8n need: deterministic pre-production checks for workflows created by people or agents.
+
+### Interests
+
+Select both:
+
+- **Codex Security**
+- **API credits for my project**
+
+### OpenAI Organization ID
+
+`[FILL PRIVATELY: org-...]`
+
+Use the organization that should receive the API credits. Confirm it in the OpenAI Platform before submitting; do not place it in this public document.
### How will API credits be used?
-> Credits will support OSS maintenance: generate adversarial workflow fixtures, run reproducible policy evaluations, review contributed rules in pull requests, explain regressions, and prepare release reports. They will not fund a hosted commercial runtime or process private customer workflows.
+Character count: **390/500**.
-### Anything else?
+> API credits would power OSS maintenance workflows: turn anonymized failures into adversarial fixtures, evaluate new policies, reproduce false positives, review contributed rules, triage issues, and generate release audits. Codex would assist these workflows; the Vibeflow scanner will remain local, dependency-free, telemetry-free, and will not upload or process private customer workflows.
-> Vibeflow comes from production lessons operating conversational automations in Brazil. It is deliberately interoperable with n8n and existing MCP tooling: it does not replace builders, it checks their output. The project ships without telemetry and keeps workflow analysis local.
+### Anything else we should know?
-## Final verification
+Character count: **431/500**.
-Before submitting, capture a fresh evidence snapshot, confirm the GitHub profile and repository remain public, verify each form answer remains under 500 characters, and use accurate current evidence only. Replace the deferral sentence only after the practical submission gate is met.
+> Vibeflow is a public case study in Codex-native OSS development. Codex helped reposition an old prototype, implement the CLI, run repeated Red Team reviews, fix regressions, package a GitHub Action and Codex plugin, open and merge PRs, and publish v0.8.0. Its first production-scale audit exposed workflow risks and analyzer limitations, creating a concrete maintenance backlog. Adoption is early; the engineering evidence is real.
+
+## PT-BR review translation
+
+These translations are for review only. Paste the English versions above into the form.
+
+### Repository eligibility
+
+> Vibeflow Γ© um gate MIT de seguranΓ§a e contratos para workflows n8n gerados por IA, reconstruΓdo de ponta a ponta com Codex. O plugin v0.8.0 auditou em modo somente leitura um workflow real de 92 nΓ³s e produziu resultados com proveniΓͺncia: 3 bloqueios de seguranΓ§a de IA, 57 avisos priorizados e gates concretos de revalidaΓ§Γ£o. Ele atende a uma necessidade crescente do ecossistema n8n: checks determinΓsticos antes da produΓ§Γ£o para workflows criados por pessoas ou agentes.
+
+### API-credit use
+
+> Os crΓ©ditos financiariam fluxos de manutenΓ§Γ£o OSS: transformar falhas anonimizadas em fixtures adversariais, avaliar novas polΓticas, reproduzir falsos positivos, revisar regras contribuΓdas, triar issues e gerar auditorias de release. O Codex auxiliaria esses fluxos; o scanner Vibeflow permanecerΓ‘ local, sem dependΓͺncias ou telemetria, e nΓ£o enviarΓ‘ nem processarΓ‘ workflows privados de clientes.
+
+### Additional context
+
+> Vibeflow Γ© um estudo de caso pΓΊblico de desenvolvimento open source nativo em Codex. O Codex ajudou a reposicionar um protΓ³tipo antigo, implementar a CLI, executar Red Teams repetidos, corrigir regressΓ΅es, empacotar uma GitHub Action e um plugin Codex, mesclar PRs e publicar a v0.8.0. A primeira auditoria em escala de produΓ§Γ£o revelou riscos do workflow e limitaΓ§Γ΅es do analisador, criando um backlog concreto. A adoΓ§Γ£o ainda Γ© inicial; a evidΓͺncia de engenharia Γ© real.
+
+## Evidence map
+
+Use these links only if OpenAI requests verification; the form has no dedicated evidence field.
+
+- Public MIT repository:
+- Executable release:
+- Reproducible demo:
+- Release and Red Team audit:
+- CI history:
+- Launch discussion:
+- Engineering PRs:
+
+## Evidence snapshot
+
+Captured on 2026-07-22:
+
+- public repository with MIT license;
+- `v0.8.0` release;
+- 26 adversarial tests on Node.js 20, 22, and 24;
+- dependency-free CLI, GitHub Action, and installable Codex plugin;
+- 3 maintainer PRs merged with green CI;
+- 2 stars, 0 forks, and no verified external contributor yet;
+- first real audit: anonymized 92-node workflow, 3 blocking findings, 57 warnings, no workflow mutation;
+- Codex used across product repositioning, implementation, review, Red Team, remediation, packaging, CI, release, and real-workflow audit.
+
+Do not describe maintainer PRs, the maintainer's own workflow, clones, or unattributed stars as external adoption.
+
+## Confidentiality boundary
+
+Do not submit or link the full real-workflow audit. Application materials must not include:
+
+- client, clinic, or workflow names;
+- remote workflow IDs, version IDs, hashes, or timestamps;
+- node names that reveal business logic;
+- private workflow JSON, credentials, customer data, or infrastructure identifiers.
+
+The approved public description is: **βa real, production-scale 92-node conversational workflow audited in read-only mode.β**
+
+## Submission checklist
+
+- [x] Repository is public and not archived.
+- [x] GitHub username is public.
+- [x] Role is principal maintainer.
+- [x] Repository URL is correct.
+- [x] Both requested benefits are selected.
+- [x] All narrative answers are under 500 characters.
+- [x] Real-workflow evidence is anonymized.
+- [x] Early adoption is described honestly.
+- [ ] Insert the exact ChatGPT-account email.
+- [ ] Insert and verify the OpenAI Organization ID.
+- [ ] Re-read the current Program Terms immediately before submission.
+- [ ] Save the confirmation page and submission timestamp privately.
+
+## After submission
+
+Continue collecting organic evidence without delaying the application:
+
+1. publish the r/n8n post;
+2. invite users to report anonymized false positives and missed unsafe cases;
+3. respond to every issue or discussion with reproducible evidence;
+4. keep releases, CI, and the public maintenance trail current;
+5. never request artificial stars, forks, or empty PRs.
+
+OpenAI reviews applications continuously. If additional evidence is requested, provide a fresh public snapshot and the anonymized outcome of subsequent real-world audits.
From f62a78faaf5cfe31b3668aed3e67b24a5276e2c2 Mon Sep 17 00:00:00 2001
From: Felipe Domingues <53182096+domfelipe@users.noreply.github.com>
Date: Wed, 22 Jul 2026 13:58:46 -0300
Subject: [PATCH 5/9] Add outcome-aware preflight contracts for v0.9 (#6)
Add VF010-VF013, structural outcome contracts, adversarial fixtures, documentation, and Friday release materials.
---
.vibeflow.json | 6 +-
CHANGELOG.md | 8 +
README.md | 49 +-
docs/architecture.md | 14 +
docs/codex-for-oss-application.md | 28 +-
docs/demo.md | 17 +
docs/launch.md | 32 +-
docs/product-brief.md | 12 +-
docs/release-audit.md | 86 ++--
docs/release-notes-v0.9.md | 24 +
docs/roadmap.md | 8 +-
examples/outcome-contracts.vibeflow.json | 14 +
examples/safe-refund.workflow.json | 85 ++++
examples/safe-support-agent.workflow.json | 15 +-
examples/unsafe-refund.workflow.json | 29 ++
package-lock.json | 4 +-
package.json | 2 +-
plugins/vibeflow/.codex-plugin/plugin.json | 6 +-
plugins/vibeflow/skills/vibeflow/SKILL.md | 6 +-
.../skills/vibeflow/references/policies.md | 6 +
schemas/vibeflow-config.schema.json | 69 ++-
src/vibeflow.mjs | 417 +++++++++++++++++-
test/vibeflow.test.mjs | 265 ++++++++++-
23 files changed, 1107 insertions(+), 95 deletions(-)
create mode 100644 docs/release-notes-v0.9.md
create mode 100644 examples/outcome-contracts.vibeflow.json
create mode 100644 examples/safe-refund.workflow.json
create mode 100644 examples/unsafe-refund.workflow.json
diff --git a/.vibeflow.json b/.vibeflow.json
index 93fd227..09bfb6f 100644
--- a/.vibeflow.json
+++ b/.vibeflow.json
@@ -9,6 +9,10 @@
"VF006": "error",
"VF007": "warning",
"VF008": "warning",
- "VF009": "warning"
+ "VF009": "warning",
+ "VF010": "error",
+ "VF011": "warning",
+ "VF012": "warning",
+ "VF013": "warning"
}
}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 367307e..c1b485f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,13 @@
# Changelog
+## 0.9.0 - 2026-07-24
+
+- Added outcome-aware policies VF010-VF013 for money, customer, privileged, and destructive-data actions.
+- Added structurally verified outcome contracts for approval, durable audit, idempotency, amount and counterparty limits, failure notification, and recovery.
+- Added detection of silent connected error paths and high-confidence classification of ordinary nodes such as HTTP requests by their real-world action.
+- Added safe and unsafe refund fixtures, configuration schema support, Codex plugin guidance, and adversarial regression tests.
+- Documented the static-analysis ceiling: runtime authorization, limits, audit durability, and recovery still belong in the executing system.
+
## 0.8.0 - 2026-07-22
- Repositioned Vibeflow as a safety and contract gate for AI-generated n8n workflows.
diff --git a/README.md b/README.md
index 5b85a6a..3f44f04 100644
--- a/README.md
+++ b/README.md
@@ -7,14 +7,14 @@
Vibeflow answers one question before deployment: **does this workflow deserve to reach production?**
-It inspects exported n8n JSON for embedded secrets, dangerous nodes, exposed webhooks, missing failure paths, absent idempotency, unsafe AI paths, unbounded execution, and risky retries.
+It inspects exported n8n JSON for embedded secrets, dangerous nodes, exposed webhooks, missing failure paths, absent idempotency, unsafe AI paths, unbounded execution, risky retries, and unguarded real-world outcomes such as refunds, customer messages, privileged actions, and destructive writes.
Vibeflow is not another workflow builder or MCP server. It is a deterministic quality gate for workflows built by people or agents.
## Quick start
```bash
-npx --yes github:domfelipe/vibeflow-n8n#4998605ed7dc12b9b867d69d7005d25778c7e109 check workflow.json
+npx --yes github:domfelipe/vibeflow-n8n#v0.9.0 check workflow.json
```
Or from a checkout:
@@ -55,9 +55,44 @@ node bin/vibeflow.mjs check examples/unsafe-support-agent.workflow.json --fail-o
| VF007 | warning | AI paths without a reachable external human handoff |
| VF008 | warning | Workflows without a 1-3600 second execution timeout |
| VF009 | warning | Retries without idempotency, bounds, or backoff |
+| VF010 | error | Money or privileged actions without a verified outcome contract |
+| VF011 | warning | Customer communications or destructive writes without an outcome contract |
+| VF012 | warning | Connected error paths that terminate without operator notification |
+| VF013 | warning | High-impact writes without compensation, rollback, or replay evidence |
Static analysis cannot prove runtime correctness. Configure severity and domain vocabulary in `.vibeflow.json`; document every waiver.
+## Outcome contracts
+
+Vibeflow v0.9 separates a dangerous node from a dangerous outcome. A normal HTTP node can issue a refund, notify a customer, or change access. High-confidence cases are detected from the action's name, type, operation, URL, and parameters. Teams can classify other actions explicitly.
+
+Contracts are keyed by the exact action node name:
+
+```json
+{
+ "outcomeContracts": {
+ "Issue refund": {
+ "impact": "money",
+ "approvalNode": "Approve refund",
+ "auditNode": "Record refund audit",
+ "failureNotificationNode": "Notify refund failure",
+ "amountGuard": { "node": "Limit refund amount", "maximum": 500, "currency": "USD" },
+ "counterpartyGuard": { "node": "Allow refund account", "allowed": ["merchant-primary"] },
+ "recovery": { "strategy": "compensate", "node": "Compensate transaction" }
+ }
+ }
+}
+```
+
+The checker verifies that referenced nodes exist, required gates dominate every entry path, approval/limit allow and deny branches are structural, a durable audit happens before the action, an atomic idempotency claim cannot be bypassed, failure notification is on the error branch, and recovery is represented in the graph.
+
+Run the outcome demo:
+
+```bash
+node bin/vibeflow.mjs check examples/unsafe-refund.workflow.json --fail-on never
+node bin/vibeflow.mjs check examples/safe-refund.workflow.json --config examples/outcome-contracts.vibeflow.json
+```
+
Use `--locked` in untrusted CI. It rejects disabled or downgraded rules, changed vocabulary, and removal of default banned node types. The bundled GitHub Action always enables it.
## Automation
@@ -74,7 +109,7 @@ Directories are searched recursively for `*.workflow.json` files.
```yaml
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
-- uses: domfelipe/vibeflow-n8n@4998605ed7dc12b9b867d69d7005d25778c7e109 # v0.8.0 code
+- uses: domfelipe/vibeflow-n8n@v0.9.0
with:
path: workflows/
output: vibeflow.sarif
@@ -83,7 +118,7 @@ Directories are searched recursively for `*.workflow.json` files.
## Codex plugin
```bash
-codex plugin marketplace add domfelipe/vibeflow-n8n --ref 4998605ed7dc12b9b867d69d7005d25778c7e109
+codex plugin marketplace add domfelipe/vibeflow-n8n --ref v0.9.0
```
Install **Vibeflow** from the Plugins Directory, then ask:
@@ -96,17 +131,19 @@ Use $vibeflow to audit this n8n workflow and fix blocking findings.
No hosted service, new MCP server, workflow generation, telemetry, secret collection, or live n8n mutation. The CLI uses only the Node.js 20+ standard library.
+An outcome contract is structural evidence, not runtime enforcement. Money and customer-facing actions still need server-side amount and counterparty checks, atomic idempotency, approval authorization, durable audit storage, and tested recovery behavior at runtime.
+
## Documentation
- [Product brief](docs/product-brief.md)
- [Architecture and limitations](docs/architecture.md)
- [Reproducible demo](docs/demo.md)
-- [v0.8.0 release audit](docs/release-audit.md)
+- [Release audit](docs/release-audit.md)
- [Roadmap](docs/roadmap.md)
- [Codex for Open Source application gate](docs/codex-for-oss-application.md)
- [Contributing](CONTRIBUTING.md)
- [Security](SECURITY.md)
-`v0.8.0` is the first executable release. The original documentation prototype is preserved at `legacy-v0.7.0`.
+`v0.9.0` adds outcome-aware preflight checks. `v0.8.0` remains the first executable release, and the original documentation prototype is preserved at `legacy-v0.7.0`.
MIT licensed.
diff --git a/docs/architecture.md b/docs/architecture.md
index b666629..c4ab728 100644
--- a/docs/architecture.md
+++ b/docs/architecture.md
@@ -24,6 +24,18 @@ Rules have a stable ID, default severity, description, and remediation. Trusted
Graph policies use the validated n8n `main` connection shape, not node order; AI resource wiring is not control flow. VF005 requires a high-confidence atomic ledger gate that emits no item for duplicate claims and dominates inbound paths to side effects. VF006 rejects any AI entry path that bypasses a positive IF check against a direct agent-status reference. VF007 requires a reachable external handoff action.
+VF010-VF013 introduce outcome contracts. High-confidence classification inspects a side-effect node's name, type, operation, URL, and parameters for money, customer communication, privileged access, and destructive-data semantics. Explicit `outcomeContracts` cover domain actions the built-in vocabulary does not recognize.
+
+For a contracted action, the analyzer verifies graph evidence rather than labels alone:
+
+- an atomic idempotency claim dominates the action;
+- durable audit storage dominates the action;
+- approval, amount, and counterparty guards are IF/Switch nodes with both an allowed route and a route that cannot reach the action;
+- the declared failure notification is reachable from the action's error output;
+- compensation and rollback nodes are downstream from the action; replay nodes must exist and carry recovery semantics.
+
+Money and privileged outcomes default to blocking VF010. Customer communications and destructive writes default to advisory VF011. Missing recovery and silent error paths are separate findings so teams can triage them independently.
+
`--locked` rejects configuration that weakens built-in severity, changes safety vocabulary, or removes a default banned node. The GitHub Action always enables locked mode so a pull request cannot silence its own findings by editing `.vibeflow.json`.
## Trust boundaries
@@ -35,6 +47,8 @@ Output paths are selected by the caller. The GitHub Action passes inputs as quot
## Known limitations
- Static structure cannot prove that a condition, SQL claim, or handoff works at runtime.
+- Outcome contracts cannot prove caller authorization, amount calculation, counterparty identity, audit durability, or successful compensation at runtime.
+- Automatic outcome classification is intentionally conservative and can miss domain-specific actions; declare them explicitly in configuration.
- Secret detection can miss unusual key names and can produce false positives.
- Community nodes unknown to the side-effect catalog need explicit policy additions.
- Locked mode protects policy content, but repository owners must still review changes to the CI workflow itself.
diff --git a/docs/codex-for-oss-application.md b/docs/codex-for-oss-application.md
index ba062db..75710a0 100644
--- a/docs/codex-for-oss-application.md
+++ b/docs/codex-for-oss-application.md
@@ -12,7 +12,7 @@ Prepared: 2026-07-22
Submit in **English**, even through the PT-BR form. OpenAI publishes no language requirement and no evidence that language changes selection odds. English is recommended only to reduce translation friction for a global technical review.
-Submit now. Stars, forks, downloads, repository usage, ecosystem importance, and active maintenance are evaluation signals, not published minimum thresholds. Vibeflow's strongest evidence is its Codex-native engineering and its first real 92-node audit, not early popularity metrics.
+Submit immediately after the v0.9.0 release on 2026-07-24. Do not wait for arbitrary star, fork, or PR targets. Vibeflow's strongest evidence is its Codex-native engineering, first real 92-node audit, and a public maintenance loop that turned community feedback into tested policy features.
## Copy-and-paste form
@@ -42,9 +42,9 @@ Select: **Principal maintainer**.
### Why is this repository eligible?
-Character count: **420/500**.
+Recount before submission; keep under 500 characters.
-> Vibeflow is an MIT-licensed safety and contract gate for AI-generated n8n workflows, rebuilt end-to-end with Codex. Its v0.8.0 plugin audited a real 92-node workflow in read-only mode and produced provenance-backed results: 3 blocking AI-safety findings, 57 prioritized warnings, and concrete revalidation gates. It fills a growing n8n need: deterministic pre-production checks for workflows created by people or agents.
+> Vibeflow is an MIT-licensed safety gate for AI-generated n8n workflows, rebuilt end-to-end with Codex. It audited a real 92-node workflow in read-only mode, finding 3 blocking risks and 57 warnings. Community feedback then shaped v0.9: outcome-aware checks for refunds, customer actions, silent failures, audit, idempotency, approval, limits, and recovery.
### Interests
@@ -67,9 +67,9 @@ Character count: **390/500**.
### Anything else we should know?
-Character count: **431/500**.
+Recount before submission; keep under 500 characters.
-> Vibeflow is a public case study in Codex-native OSS development. Codex helped reposition an old prototype, implement the CLI, run repeated Red Team reviews, fix regressions, package a GitHub Action and Codex plugin, open and merge PRs, and publish v0.8.0. Its first production-scale audit exposed workflow risks and analyzer limitations, creating a concrete maintenance backlog. Adoption is early; the engineering evidence is real.
+> Vibeflow is a public case study in Codex-native OSS development. Codex drove the project from product repositioning through implementation, adversarial review, remediation, CI, plugin packaging, releases, a real-workflow audit, and the v0.9 response to user feedback. Adoption is early, but the engineering, maintenance history, and real-world evidence are public and reproducible.
## PT-BR review translation
@@ -77,7 +77,7 @@ These translations are for review only. Paste the English versions above into th
### Repository eligibility
-> Vibeflow Γ© um gate MIT de seguranΓ§a e contratos para workflows n8n gerados por IA, reconstruΓdo de ponta a ponta com Codex. O plugin v0.8.0 auditou em modo somente leitura um workflow real de 92 nΓ³s e produziu resultados com proveniΓͺncia: 3 bloqueios de seguranΓ§a de IA, 57 avisos priorizados e gates concretos de revalidaΓ§Γ£o. Ele atende a uma necessidade crescente do ecossistema n8n: checks determinΓsticos antes da produΓ§Γ£o para workflows criados por pessoas ou agentes.
+> Vibeflow Γ© um gate MIT de seguranΓ§a para workflows n8n gerados por IA, reconstruΓdo de ponta a ponta com Codex. Ele auditou em modo somente leitura um workflow real de 92 nΓ³s, encontrando 3 riscos bloqueantes e 57 avisos. O feedback da comunidade entΓ£o moldou a v0.9: checks de resultados reais para reembolsos, aΓ§Γ΅es com clientes, falhas silenciosas, auditoria, idempotΓͺncia, aprovaΓ§Γ£o, limites e recuperaΓ§Γ£o.
### API-credit use
@@ -85,14 +85,14 @@ These translations are for review only. Paste the English versions above into th
### Additional context
-> Vibeflow Γ© um estudo de caso pΓΊblico de desenvolvimento open source nativo em Codex. O Codex ajudou a reposicionar um protΓ³tipo antigo, implementar a CLI, executar Red Teams repetidos, corrigir regressΓ΅es, empacotar uma GitHub Action e um plugin Codex, mesclar PRs e publicar a v0.8.0. A primeira auditoria em escala de produΓ§Γ£o revelou riscos do workflow e limitaΓ§Γ΅es do analisador, criando um backlog concreto. A adoΓ§Γ£o ainda Γ© inicial; a evidΓͺncia de engenharia Γ© real.
+> Vibeflow Γ© um estudo de caso pΓΊblico de desenvolvimento open source nativo em Codex. O Codex conduziu o projeto desde o reposicionamento e implementaΓ§Γ£o atΓ© revisΓ£o adversarial, correΓ§Γ΅es, CI, plugin, releases, auditoria de workflow real e a resposta da v0.9 ao feedback de usuΓ‘rios. A adoΓ§Γ£o ainda Γ© inicial, mas a engenharia, o histΓ³rico de manutenΓ§Γ£o e a evidΓͺncia real sΓ£o pΓΊblicos e reproduzΓveis.
## Evidence map
Use these links only if OpenAI requests verification; the form has no dedicated evidence field.
- Public MIT repository:
-- Executable release:
+- Executable release after Friday launch:
- Reproducible demo:
- Release and Red Team audit:
- CI history:
@@ -101,16 +101,16 @@ Use these links only if OpenAI requests verification; the form has no dedicated
## Evidence snapshot
-Captured on 2026-07-22:
+Refresh this section on 2026-07-24 immediately before submission. Current candidate evidence:
- public repository with MIT license;
-- `v0.8.0` release;
-- 26 adversarial tests on Node.js 20, 22, and 24;
+- `v0.9.0` release candidate, with v0.8.0 already public;
+- 36 adversarial tests passing locally and in remote Node.js 20, 22, and 24 CI;
- dependency-free CLI, GitHub Action, and installable Codex plugin;
-- 3 maintainer PRs merged with green CI;
+- 3 prior maintainer PRs merged with green CI; add the v0.9 PR after merge;
- 2 stars, 0 forks, and no verified external contributor yet;
- first real audit: anonymized 92-node workflow, 3 blocking findings, 57 warnings, no workflow mutation;
-- Codex used across product repositioning, implementation, review, Red Team, remediation, packaging, CI, release, and real-workflow audit.
+- Codex used across product repositioning, implementation, review, Red Team, remediation, packaging, CI, release, real-workflow audit, and the community-feedback-driven v0.9 cycle.
Do not describe maintainer PRs, the maintainer's own workflow, clones, or unattributed stars as external adoption.
@@ -144,7 +144,7 @@ The approved public description is: **βa real, production-scale 92-node conver
Continue collecting organic evidence without delaying the application:
-1. publish the r/n8n post;
+1. publish the v0.9 follow-up in r/n8n;
2. invite users to report anonymized false positives and missed unsafe cases;
3. respond to every issue or discussion with reproducible evidence;
4. keep releases, CI, and the public maintenance trail current;
diff --git a/docs/demo.md b/docs/demo.md
index e47b5fe..e63e36d 100644
--- a/docs/demo.md
+++ b/docs/demo.md
@@ -16,6 +16,23 @@ node bin/vibeflow.mjs check examples/unsafe-support-agent.workflow.json --fail-o
Expected result: VF001-VF009 findings covering secrets, dangerous nodes, webhook authentication, error handling, idempotency, AI safety, timeouts, and retries.
+## Dangerous outcome
+
+```bash
+node bin/vibeflow.mjs check examples/unsafe-refund.workflow.json --fail-on never
+```
+
+Expected result: the ordinary HTTP refund node triggers blocking VF010 plus VF012 and VF013, even though its node type is not inherently dangerous.
+
+## Contracted outcome
+
+```bash
+node bin/vibeflow.mjs check examples/safe-refund.workflow.json \
+ --config examples/outcome-contracts.vibeflow.json
+```
+
+Expected result: exit `0`, zero findings. The graph contains a dominating atomic claim and durable audit, structural approval/amount/counterparty gates, error notification, and compensation evidence.
+
## Automation output
```bash
diff --git a/docs/launch.md b/docs/launch.md
index 2c24a99..35eefd2 100644
--- a/docs/launch.md
+++ b/docs/launch.md
@@ -1,22 +1,28 @@
-# Launch checklist
+# v0.9.0 launch checklist
+
+Target: Friday, 2026-07-24
## Release gate
-- [x] `npm run verify` passes on Node.js 20, 22, and 24 in CI.
-- [x] Official skill and plugin validators pass.
-- [x] Safe fixture exits 0; unsafe fixture exits 1.
-- [x] SARIF is valid JSON and uploaded by CI.
-- [x] Repository description and topics match the new product.
-- [x] `v0.8.0` release notes match `CHANGELOG.md`.
-- [x] Remote CLI and pinned Codex marketplace install successfully.
-- [x] Private vulnerability reporting and Discussions are enabled.
+- [x] VF010-VF013 implementation and configuration schema are complete.
+- [x] Safe and unsafe refund fixtures are reproducible.
+- [x] Local `npm run verify` and `npm audit --omit=dev` pass.
+- [x] QA, adversarial Red Team, and GuardiΓ£o reviews are documented.
+- [x] Pull request CI passes on Node.js 20, 22, and 24.
+- [ ] Release commit is merged and tagged `v0.9.0`.
+- [ ] Released CLI and pinned Codex marketplace install successfully.
+- [ ] GitHub release is published on Friday.
-Release:
+## Positioning
-Launch discussion:
+The launch message is: **dangerous outcomes are not limited to dangerous nodes**.
+
+A normal HTTP, database, or messaging node can refund money, contact a customer, change access, or destroy data. Vibeflow v0.9 adds a preflight contract for the controls that should surround those outcomes: atomic idempotency, approval, amount and counterparty limits, durable audit, operator-visible failure paths, and recovery.
## Announcement
-> Vibeflow is now an executable safety gate for AI-generated n8n workflows. It checks exported JSON for secrets, exposed webhooks, missing kill switches and handoffs, idempotency, failure paths, timeouts, and unsafe retries. It is dependency-free, runs locally or in GitHub Actions, and includes a Codex plugin.
+> Vibeflow v0.9 asks a more useful preflight question for generated n8n workflows: not only βis this valid JSON?β or βdoes it use a dangerous node?β, but βwhat can this workflow do in the real world if the input is messy or the model is wrong?β
+>
+> The new outcome contracts detect money, customer, privileged, and destructive-data actions β including ordinary HTTP nodes β and verify structural evidence for idempotency, approval, limits, durable audit, failure notification, and recovery. It is local, dependency-free, CI-friendly, and explicit about what still needs runtime enforcement.
-Link to the repository and the safe/unsafe demo. Ask users for anonymized false-positive cases and real workflow fixtures, not stars alone.
+Link to the repository and the safe/unsafe refund demo. Ask users for anonymized workflows, classifier false positives/negatives, and missing domain actions.
diff --git a/docs/product-brief.md b/docs/product-brief.md
index d9ce161..80ead6f 100644
--- a/docs/product-brief.md
+++ b/docs/product-brief.md
@@ -6,7 +6,7 @@ Vibeflow is an open-source safety and contract gate for exported n8n workflows,
## Problem
-Natural-language workflow generation is now common. The remaining failure is operational: a structurally valid workflow can still leak a credential, answer while disabled, duplicate a side effect, lack a human fallback, retry unsafely, or run forever.
+Natural-language workflow generation is now common. The remaining failure is operational: a structurally valid workflow can still leak a credential, answer while disabled, duplicate a side effect, lack a human fallback, retry unsafely, run forever, issue a refund, notify a customer, or destroy data without the required controls.
Existing builders and MCP servers should keep building. Vibeflow checks the result before production.
@@ -21,12 +21,14 @@ Existing builders and MCP servers should keep building. Vibeflow checks the resu
Given an exported workflow, produce a reproducible pass/fail report with concrete remediation and no network access.
-## Version 0.8 scope
+## Version 0.9 scope
- dependency-free Node.js CLI;
- text, JSON, and SARIF output;
-- configurable VF000-VF009 policies;
-- safe and unsafe fixtures;
+- configurable VF000-VF013 policies;
+- outcome contracts for money, customer, privileged, and destructive-data actions;
+- structural checks for approval, durable audit, idempotency, limits, failure notification, and recovery;
+- safe and unsafe support and refund fixtures;
- GitHub Action;
- Codex skill and plugin package.
@@ -40,7 +42,7 @@ Given an exported workflow, produce a reproducible pass/fail report with concret
## Differentiation
-Vibeflow starts with policies learned from real conversational-agent operations: an off switch before inference, human handoff after low-confidence output, duplicate-event protection, explicit error paths, bounded retries, and trust-boundary hygiene.
+Vibeflow starts with policies learned from real operations: an off switch before inference, human handoff after low-confidence output, duplicate-event protection, explicit error paths, bounded retries, trust-boundary hygiene, and declared controls around real-world outcomes. It asks not only whether the workflow is valid JSON or uses a suspicious node, but what it can do when input is messy or a model is wrong.
## Evidence gate
diff --git a/docs/release-audit.md b/docs/release-audit.md
index 6dc7d23..aa4a4c7 100644
--- a/docs/release-audit.md
+++ b/docs/release-audit.md
@@ -1,54 +1,70 @@
-# Release audit β v0.8.0
+# Release audit β v0.9.0 candidate
-Date: 2026-07-22
+Target release: 2026-07-24
## Decision
-**APTO COM RESSALVAS** for the first public executable release.
+**APTO COM RESSALVAS** for the Friday release, pending merge, tag, and released-install checks listed below.
-No blocking defect remains in the reviewed static-analysis boundary. The remaining caveats require runtime or node-specific knowledge that an exported JSON gate cannot prove.
+No blocking defect remains in the reviewed static-analysis boundary. Vibeflow can verify structural evidence in an exported workflow, but it cannot enforce authorization, amount limits, counterparty identity, audit durability, or recovery behavior in the executing systems.
## QA evidence
-- 26 automated tests pass, including one safe and one intentionally unsafe workflow.
-- The safe fixture produces zero findings; the unsafe fixture produces VF001-VF009.
-- Text, JSON, and SARIF output paths are exercised.
-- Node 20, 22, and 24 are required in CI; local verification used the available Node runtime and the remote matrix is the release gate.
-- JSON/YAML parsing, package dry-run, official skill/plugin validators, `npm audit`, and `git diff --check` are part of the final gate.
+- 36 automated tests pass, including safe and unsafe support workflows and a fully contracted refund workflow.
+- The unsafe refund fixture detects an ordinary HTTP node as a money outcome and produces VF010, VF012, and VF013.
+- The contracted refund fixture exits 0 in normal and `--locked` modes.
+- Text, JSON, SARIF, configuration validation, package packing, and CLI exit behavior are exercised.
+- `npm run verify`, `npm audit --omit=dev`, JSON parsing, and `git diff --check` pass locally.
+- The package remains dependency-free and targets Node.js 20+.
## Red Team
-The first implementation was rejected. Regression tests now cover the reproduced bypasses:
+The first v0.9 implementation was hardened after adversarial review. Regression tests now cover:
-- declared webhook authentication without the matching credential reference;
-- literal secrets in raw headers, URLs, expressions, pinned data, and static data;
-- disconnected, nominal, or non-gating idempotency controls;
-- parallel AI paths that bypass a kill switch, inverted or constant conditions, and decorative Code/NoOp nodes;
-- resource connections misread as control flow and fabricated connection shapes;
-- disconnected or cyclic error handling;
-- read-only HTTP requests mislabeled as handoff;
-- excessive or non-numeric timeouts and unsafe retry settings;
-- policy weakening from an untrusted checkout;
-- terminal-control injection, deep JSON, excessive nodes/edges/files/config terms, finding amplification, and quadratic traversal.
+1. ordinary HTTP refunds that would evade a dangerous-node-only policy;
+2. read-only refund queries, preventing an obvious classifier false positive;
+3. named or disconnected approval, amount, counterparty, audit, and idempotency controls;
+4. constant approval conditions and decorative amount values;
+5. read-only or fail-open audit nodes, including an audit error branch that still reaches the action;
+6. fail-open idempotency nodes and idempotency error branches that still reach the side effect;
+7. prototype-like contract keys, self-referential notification/recovery evidence, and audit-like labels used to hide a money action;
+8. connected error paths that terminate without a recognized operator notification.
-At the 5,000-node limit, the corrected linear traversal completed the synthetic chain in tens of milliseconds on the development machine. Finding truncation always adds blocking VF000.
+The graph checks use actual `main` edges, require dominating controls, and reject evidence that reaches the action after the control itself fails.
-## Supply chain
+## GuardiΓ£o security review
-- GitHub-owned actions are pinned to full verified commit SHAs.
-- Checkout credentials are not persisted; workflow permissions are `contents: read`.
-- Jobs have a ten-minute timeout and package installation ignores scripts.
-- The package has no runtime dependencies and uses a publish allowlist.
-- The bundled action always enables `--locked`.
+### In scope
-The first merge SHA, `4998605ed7dc12b9b867d69d7005d25778c7e109`, pins the CLI, GitHub Action, and Codex marketplace examples before the release tag is created.
+- local CLI parsing of untrusted workflow/configuration JSON;
+- graph and parameter analysis;
+- text, JSON, and SARIF output;
+- npm package contents, GitHub Action boundary, and Codex plugin instructions.
-## Residual limitations
+### Controls confirmed
-- Static analysis cannot prove a referenced credential exists or works.
-- SQL and IF checks are high-confidence structural evidence, not runtime execution proofs.
-- An HTTP POST labeled as a ticket or handoff may still fail or target the wrong service.
-- Unknown community nodes may require a new side-effect adapter and regression fixture.
-- Repository owners must still review changes to the CI workflow itself.
+- no runtime dependencies, telemetry, hosted service, or live n8n mutation;
+- no workflow expressions or embedded code are evaluated;
+- no URLs found in a workflow are contacted;
+- file, node, edge, contract, vocabulary, and finding budgets are bounded;
+- terminal text is sanitized and prototype-like contract keys are handled as data;
+- the GitHub Action uses locked policy mode so a pull request cannot disable its own built-in checks;
+- examples contain credential references and reserved invalid domains, not live secrets.
-The official Codex Security workspace was opened, but its setup was never submitted through the app interface; no result from that scanner is claimed here. The release decision is based on the direct QA, independent Red Team, supply-chain review, and regression evidence above.
+### Residual limitations
+
+- an exported graph cannot prove a referenced credential, approval identity, SQL policy, external API limit, notification, or compensation works at runtime;
+- custom/community nodes may need explicit impact declarations or new regression-backed adapters;
+- repository owners may intentionally weaken policy outside `--locked` mode;
+- remote Node 20/22/24 CI passed; released `npx`/plugin installation can only be confirmed after the candidate is tagged.
+
+## Release blockers
+
+- [x] Pull request CI passes on Node.js 20, 22, and 24.
+- [ ] Release commit is merged without unrelated changes.
+- [ ] `v0.9.0` tag and GitHub release are published on 2026-07-24.
+- [ ] Released CLI and Codex plugin install paths are smoke-tested.
+
+## Final gate
+
+There are zero known critical or high security findings in the candidate. The release remains **APTO COM RESSALVAS** until the remaining remote gates above are complete; a failed gate blocks publication or requires an immediate corrective release.
diff --git a/docs/release-notes-v0.9.md b/docs/release-notes-v0.9.md
new file mode 100644
index 0000000..c4a3198
--- /dev/null
+++ b/docs/release-notes-v0.9.md
@@ -0,0 +1,24 @@
+# Vibeflow v0.9.0 β Outcome-aware preflight for n8n
+
+Generated workflows need more than valid JSON. A normal HTTP node can still issue a refund, notify a customer, change access, or delete production data.
+
+Vibeflow v0.9 introduces outcome contracts and four policies:
+
+- **VF010** blocks uncontracted money and privileged actions.
+- **VF011** warns on uncontracted customer communications and destructive writes.
+- **VF012** detects connected error paths that notify nobody.
+- **VF013** requires a compensation, rollback, or replay story.
+
+For contracted actions, Vibeflow checks whether atomic idempotency and durable audit dominate the action, approval and limit nodes have real allow/deny branches, failure notification is connected to the error output, and recovery is represented in the graph.
+
+Try the unsafe case:
+
+```bash
+npx --yes github:domfelipe/vibeflow-n8n#v0.9.0 check examples/unsafe-refund.workflow.json --fail-on never
+```
+
+Then inspect the passing contract in `examples/outcome-contracts.vibeflow.json` and `examples/safe-refund.workflow.json`.
+
+This is a static preflight, not a runtime policy engine. Authorization, amount/counterparty enforcement, audit durability, and tested recovery still belong in the services that execute the action.
+
+Full changelog:
diff --git a/docs/roadmap.md b/docs/roadmap.md
index d9ad87a..91ce4ac 100644
--- a/docs/roadmap.md
+++ b/docs/roadmap.md
@@ -4,17 +4,23 @@
Ship the executable reset: CLI, nine configurable policies, fixtures, tests, SARIF, GitHub Action, and Codex plugin.
+## 0.9.0 β target 2026-07-24
+
+Separate dangerous nodes from dangerous outcomes. Add VF010-VF013, explicit outcome contracts, graph evidence for policy gates, safe/unsafe refund fixtures, and clear runtime boundaries.
+
## Next release gate
Do not add another integration by default. Prioritize evidence from real workflows:
-1. Measure false positives by rule.
+1. Measure false positives by rule and impact classifier.
2. Accept anonymized fixtures from external users.
3. Add a node type or policy only with a failing fixture.
4. Improve GitHub annotations if SARIF users request it.
5. Package for npm only when GitHub installation creates material friction.
6. Add node-specific handoff and side-effect adapters only with adversarial safe/unsafe fixtures.
7. Model additional atomic idempotency gates only when their duplicate path is proven to stop downstream items.
+8. Add impact categories and adapters only with a real anonymized workflow and an adversarial fixture.
+9. Explore optional runtime attestations only after users demonstrate that static contracts are insufficient.
## Explicitly deferred
diff --git a/examples/outcome-contracts.vibeflow.json b/examples/outcome-contracts.vibeflow.json
new file mode 100644
index 0000000..362f4c6
--- /dev/null
+++ b/examples/outcome-contracts.vibeflow.json
@@ -0,0 +1,14 @@
+{
+ "$schema": "../schemas/vibeflow-config.schema.json",
+ "outcomeContracts": {
+ "Issue refund": {
+ "impact": "money",
+ "approvalNode": "Approve refund",
+ "auditNode": "Record refund audit",
+ "failureNotificationNode": "Notify refund failure",
+ "amountGuard": { "node": "Limit refund amount", "maximum": 500, "currency": "USD" },
+ "counterpartyGuard": { "node": "Allow refund account", "allowed": ["merchant-primary"] },
+ "recovery": { "strategy": "compensate", "node": "Compensate transaction" }
+ }
+ }
+}
diff --git a/examples/safe-refund.workflow.json b/examples/safe-refund.workflow.json
new file mode 100644
index 0000000..7d0e225
--- /dev/null
+++ b/examples/safe-refund.workflow.json
@@ -0,0 +1,85 @@
+{
+ "name": "Safe refund with outcome contract",
+ "nodes": [
+ {
+ "id": "webhook",
+ "name": "Webhook",
+ "type": "n8n-nodes-base.webhook",
+ "parameters": { "authentication": "headerAuth", "path": "refund" },
+ "credentials": { "httpHeaderAuth": { "id": "credential-reference", "name": "Webhook Header Auth" } }
+ },
+ {
+ "id": "idempotency",
+ "name": "Claim idempotency event",
+ "type": "n8n-nodes-base.postgres",
+ "parameters": {
+ "operation": "executeQuery",
+ "query": "INSERT INTO event_ledger (event_id) VALUES ($1) ON CONFLICT (event_id) DO NOTHING RETURNING event_id"
+ }
+ },
+ {
+ "id": "audit",
+ "name": "Record refund audit",
+ "type": "n8n-nodes-base.postgres",
+ "parameters": { "operation": "insert", "table": "refund_audit" }
+ },
+ {
+ "id": "approval",
+ "name": "Approve refund",
+ "type": "n8n-nodes-base.if",
+ "parameters": { "conditions": { "boolean": [{ "value1": "={{ $json.approved }}", "value2": true }] } }
+ },
+ {
+ "id": "amount",
+ "name": "Limit refund amount",
+ "type": "n8n-nodes-base.if",
+ "parameters": {
+ "conditions": { "number": [{ "value1": "={{ $json.amount }}", "operation": "smallerEqual", "value2": 500 }] },
+ "currency": "USD"
+ }
+ },
+ {
+ "id": "counterparty",
+ "name": "Allow refund account",
+ "type": "n8n-nodes-base.switch",
+ "parameters": { "value": "={{ $json.counterparty }}", "rules": [{ "value": "merchant-primary" }] }
+ },
+ {
+ "id": "refund",
+ "name": "Issue refund",
+ "type": "n8n-nodes-base.httpRequest",
+ "onError": "continueErrorOutput",
+ "parameters": { "method": "POST", "url": "https://payments.invalid/refund" }
+ },
+ {
+ "id": "failure-alert",
+ "name": "Notify refund failure",
+ "type": "n8n-nodes-base.slack",
+ "parameters": { "channel": "operations", "text": "Notify failure for refund {{ $json.event_id }}" }
+ },
+ {
+ "id": "compensation",
+ "name": "Compensate transaction",
+ "type": "n8n-nodes-base.httpRequest",
+ "parameters": { "method": "POST", "url": "https://payments.invalid/reverse", "operation": "compensate" }
+ }
+ ],
+ "connections": {
+ "Webhook": { "main": [[{ "node": "Claim idempotency event", "type": "main", "index": 0 }]] },
+ "Claim idempotency event": { "main": [[{ "node": "Record refund audit", "type": "main", "index": 0 }]] },
+ "Record refund audit": { "main": [[{ "node": "Approve refund", "type": "main", "index": 0 }]] },
+ "Approve refund": { "main": [[{ "node": "Limit refund amount", "type": "main", "index": 0 }], []] },
+ "Limit refund amount": { "main": [[{ "node": "Allow refund account", "type": "main", "index": 0 }], []] },
+ "Allow refund account": { "main": [[{ "node": "Issue refund", "type": "main", "index": 0 }], []] },
+ "Issue refund": {
+ "main": [
+ [],
+ [
+ { "node": "Notify refund failure", "type": "main", "index": 0 },
+ { "node": "Compensate transaction", "type": "main", "index": 0 }
+ ]
+ ]
+ }
+ },
+ "settings": { "executionTimeout": 120, "errorWorkflow": "global-error-handler" }
+}
diff --git a/examples/safe-support-agent.workflow.json b/examples/safe-support-agent.workflow.json
index d9f1d3d..1a5e492 100644
--- a/examples/safe-support-agent.workflow.json
+++ b/examples/safe-support-agent.workflow.json
@@ -64,6 +64,13 @@
"type": "n8n-nodes-base.noOp",
"parameters": {}
},
+ {
+ "id": "failure-alert",
+ "name": "Operator error alert",
+ "type": "n8n-nodes-base.slack",
+ "onError": "continueErrorOutput",
+ "parameters": { "channel": "operations", "text": "Failure alert: {{ $json.error.message }}" }
+ },
{
"id": "response",
"name": "Respond to Webhook",
@@ -76,7 +83,7 @@
"Claim idempotency event": {
"main": [
[{ "node": "Agent enabled kill switch", "type": "main", "index": 0 }],
- [{ "node": "Ledger failure stop", "type": "main", "index": 0 }]
+ [{ "node": "Operator error alert", "type": "main", "index": 0 }]
]
},
"Agent enabled kill switch": { "main": [[{ "node": "AI Agent", "type": "main", "index": 0 }], []] },
@@ -89,6 +96,12 @@
]
},
"Human handoff ticket": {
+ "main": [
+ [],
+ [{ "node": "Operator error alert", "type": "main", "index": 0 }]
+ ]
+ },
+ "Operator error alert": {
"main": [
[],
[{ "node": "Handoff failure stop", "type": "main", "index": 0 }]
diff --git a/examples/unsafe-refund.workflow.json b/examples/unsafe-refund.workflow.json
new file mode 100644
index 0000000..f370e0e
--- /dev/null
+++ b/examples/unsafe-refund.workflow.json
@@ -0,0 +1,29 @@
+{
+ "name": "Unsafe refund without outcome contract",
+ "nodes": [
+ {
+ "id": "webhook",
+ "name": "Public refund webhook",
+ "type": "n8n-nodes-base.webhook",
+ "parameters": { "authentication": "none", "path": "refund" }
+ },
+ {
+ "id": "refund",
+ "name": "Issue refund",
+ "type": "n8n-nodes-base.httpRequest",
+ "onError": "continueErrorOutput",
+ "parameters": { "method": "POST", "url": "https://payments.invalid/refund" }
+ },
+ {
+ "id": "swallow",
+ "name": "Swallow refund failure",
+ "type": "n8n-nodes-base.noOp",
+ "parameters": {}
+ }
+ ],
+ "connections": {
+ "Public refund webhook": { "main": [[{ "node": "Issue refund", "type": "main", "index": 0 }]] },
+ "Issue refund": { "main": [[], [{ "node": "Swallow refund failure", "type": "main", "index": 0 }]] }
+ },
+ "settings": {}
+}
diff --git a/package-lock.json b/package-lock.json
index a6a7289..cf39e90 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "vibeflow-n8n",
- "version": "0.8.0",
+ "version": "0.9.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "vibeflow-n8n",
- "version": "0.8.0",
+ "version": "0.9.0",
"license": "MIT",
"bin": {
"vibeflow": "bin/vibeflow.mjs"
diff --git a/package.json b/package.json
index a27923b..e3b717a 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "vibeflow-n8n",
- "version": "0.8.0",
+ "version": "0.9.0",
"description": "Safety and contract checks for AI-generated n8n workflows",
"type": "module",
"bin": {
diff --git a/plugins/vibeflow/.codex-plugin/plugin.json b/plugins/vibeflow/.codex-plugin/plugin.json
index 4529029..834dd76 100644
--- a/plugins/vibeflow/.codex-plugin/plugin.json
+++ b/plugins/vibeflow/.codex-plugin/plugin.json
@@ -1,6 +1,6 @@
{
"name": "vibeflow",
- "version": "0.8.0",
+ "version": "0.9.0",
"description": "Audit and repair AI-generated n8n workflows before production",
"author": {
"name": "Felipe Domingues",
@@ -13,8 +13,8 @@
"skills": "./skills/",
"interface": {
"displayName": "Vibeflow",
- "shortDescription": "Safety checks for AI-generated n8n workflows",
- "longDescription": "Run deterministic safety and contract checks on n8n workflow exports, explain findings, and repair blocking issues before deployment.",
+ "shortDescription": "Preflight checks for real-world n8n outcomes",
+ "longDescription": "Run deterministic safety and outcome-contract checks on n8n workflow exports, explain findings, and repair blocking issues before deployment.",
"developerName": "Felipe Domingues",
"category": "Productivity",
"capabilities": ["Read", "Write"],
diff --git a/plugins/vibeflow/skills/vibeflow/SKILL.md b/plugins/vibeflow/skills/vibeflow/SKILL.md
index 16d1ca1..7e8a06c 100644
--- a/plugins/vibeflow/skills/vibeflow/SKILL.md
+++ b/plugins/vibeflow/skills/vibeflow/SKILL.md
@@ -1,6 +1,6 @@
---
name: vibeflow
-description: Audit and repair exported n8n workflow JSON with deterministic safety and contract checks. Use when reviewing AI-generated or customer-facing n8n workflows before deployment, investigating Vibeflow VF000-VF009 findings, adding kill switches or human handoffs, checking secrets, retries, and webhook exposure, or preparing workflow changes for CI and pull requests.
+description: Audit and repair exported n8n workflow JSON with deterministic safety and outcome-contract checks. Use when reviewing AI-generated or customer-facing n8n workflows before deployment, investigating Vibeflow VF000-VF013 findings, adding kill switches or human handoffs, checking secrets, retries, webhook exposure, money or customer side effects, recovery paths, or preparing workflow changes for CI and pull requests.
---
# Vibeflow
@@ -19,7 +19,7 @@ Use the CLI as the source of truth. Do not infer that a workflow is safe from it
For a released version without a checkout:
```bash
- npx --yes github:domfelipe/vibeflow-n8n#4998605ed7dc12b9b867d69d7005d25778c7e109 check path/to/workflow.json
+ npx --yes github:domfelipe/vibeflow-n8n#v0.9.0 check path/to/workflow.json
```
3. Read [references/policies.md](references/policies.md) when interpreting or repairing a finding.
@@ -32,6 +32,8 @@ Use the CLI as the source of truth. Do not infer that a workflow is safe from it
- Never copy literal credentials into a workflow to silence `VF001`.
- Never disable `VF006` for customer-facing agents without explicit user approval; an off switch must block inference and all AI responses.
- Treat static analysis as a preflight, not proof of runtime correctness.
+- Treat `VF010` and `VF011` as outcome risks, even when the underlying node type is ordinary HTTP, database, or messaging.
+- Do not claim an outcome contract enforces runtime authorization, limits, audit durability, or recovery; verify those separately in the executing system.
- Prefer fixing a shared upstream node over duplicating guards across branches.
- Keep fixes local to the exported workflow until the user authorizes deployment.
diff --git a/plugins/vibeflow/skills/vibeflow/references/policies.md b/plugins/vibeflow/skills/vibeflow/references/policies.md
index 7864016..67e7596 100644
--- a/plugins/vibeflow/skills/vibeflow/references/policies.md
+++ b/plugins/vibeflow/skills/vibeflow/references/policies.md
@@ -12,5 +12,11 @@
| VF007 | warning | AI Agent without a reachable external handoff action | Add a downstream ticket, chat, email, or equivalent human escalation action. |
| VF008 | warning | Missing or excessive execution timeout | Set the workflow timeout between 1 and 3600 seconds. |
| VF009 | warning | Unsafe retry policy | Add idempotency, bound attempts, and configure backoff. |
+| VF010 | error | Money or privileged action lacks verified policy evidence | Declare the action and connect dominating approval, durable audit, atomic idempotency, limits where applicable, failure notification, and recovery. |
+| VF011 | warning | Customer communication or destructive write lacks an outcome contract | Declare the impact and connect durable audit, atomic idempotency, failure notification, and recovery evidence. |
+| VF012 | warning | Connected error path terminates silently | Route the action's error output to a recognized operator alert, ticket, incident, or escalation node. |
+| VF013 | warning | High-impact write has no recovery contract | Declare compensation, rollback, or replay and reference the implementing node. |
Configuration changes severity or domain vocabulary; it does not prove the suppressed risk is safe. Keep waivers visible in `.vibeflow.json` and explain them in the pull request.
+
+Outcome contracts are static evidence. Runtime systems must still authorize approvers, enforce limits and counterparties server-side, create durable audit entries before acting, deduplicate atomically, and test recovery behavior.
diff --git a/schemas/vibeflow-config.schema.json b/schemas/vibeflow-config.schema.json
index c653f7e..5578fc7 100644
--- a/schemas/vibeflow-config.schema.json
+++ b/schemas/vibeflow-config.schema.json
@@ -18,7 +18,11 @@
"VF006": { "$ref": "#/$defs/severity" },
"VF007": { "$ref": "#/$defs/severity" },
"VF008": { "$ref": "#/$defs/severity" },
- "VF009": { "$ref": "#/$defs/severity" }
+ "VF009": { "$ref": "#/$defs/severity" },
+ "VF010": { "$ref": "#/$defs/severity" },
+ "VF011": { "$ref": "#/$defs/severity" },
+ "VF012": { "$ref": "#/$defs/severity" },
+ "VF013": { "$ref": "#/$defs/severity" }
}
},
"terms": {
@@ -27,7 +31,15 @@
"properties": {
"killSwitch": { "$ref": "#/$defs/terms" },
"humanHandoff": { "$ref": "#/$defs/terms" },
- "idempotency": { "$ref": "#/$defs/terms" }
+ "idempotency": { "$ref": "#/$defs/terms" },
+ "approval": { "$ref": "#/$defs/terms" },
+ "audit": { "$ref": "#/$defs/terms" },
+ "failureNotification": { "$ref": "#/$defs/terms" },
+ "outcomeMoney": { "$ref": "#/$defs/terms" },
+ "outcomeCustomer": { "$ref": "#/$defs/terms" },
+ "outcomePrivileged": { "$ref": "#/$defs/terms" },
+ "outcomeData": { "$ref": "#/$defs/terms" },
+ "recovery": { "$ref": "#/$defs/terms" }
}
},
"bannedNodeTypes": {
@@ -35,6 +47,13 @@
"items": { "type": "string", "minLength": 1, "maxLength": 200 },
"maxItems": 1000,
"uniqueItems": true
+ },
+ "outcomeContracts": {
+ "type": "object",
+ "description": "Static policy evidence keyed by the exact high-impact action node name.",
+ "maxProperties": 1000,
+ "propertyNames": { "minLength": 1, "maxLength": 200 },
+ "additionalProperties": { "$ref": "#/$defs/outcomeContract" }
}
},
"$defs": {
@@ -45,6 +64,52 @@
"minItems": 1,
"maxItems": 100,
"uniqueItems": true
+ },
+ "nodeReference": { "type": "string", "minLength": 1, "maxLength": 200 },
+ "outcomeContract": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["impact"],
+ "properties": {
+ "impact": { "enum": ["money", "customer", "privileged", "data"] },
+ "approvalNode": { "$ref": "#/$defs/nodeReference" },
+ "auditNode": { "$ref": "#/$defs/nodeReference" },
+ "failureNotificationNode": { "$ref": "#/$defs/nodeReference" },
+ "amountGuard": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["node", "maximum", "currency"],
+ "properties": {
+ "node": { "$ref": "#/$defs/nodeReference" },
+ "maximum": { "type": "number", "exclusiveMinimum": 0 },
+ "currency": { "type": "string", "pattern": "^[A-Z]{3}$" }
+ }
+ },
+ "counterpartyGuard": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["node", "allowed"],
+ "properties": {
+ "node": { "$ref": "#/$defs/nodeReference" },
+ "allowed": {
+ "type": "array",
+ "minItems": 1,
+ "maxItems": 100,
+ "uniqueItems": true,
+ "items": { "type": "string", "minLength": 1, "maxLength": 100 }
+ }
+ }
+ },
+ "recovery": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["strategy", "node"],
+ "properties": {
+ "strategy": { "enum": ["compensate", "rollback", "replay"] },
+ "node": { "$ref": "#/$defs/nodeReference" }
+ }
+ }
+ }
}
}
}
diff --git a/src/vibeflow.mjs b/src/vibeflow.mjs
index 460a53a..3ba9f09 100644
--- a/src/vibeflow.mjs
+++ b/src/vibeflow.mjs
@@ -1,7 +1,7 @@
import { access, readFile, readdir, stat } from "node:fs/promises";
import path from "node:path";
-export const VERSION = "0.8.0";
+export const VERSION = "0.9.0";
export const MAX_WORKFLOW_BYTES = 25 * 1024 * 1024;
export const MAX_CONFIG_BYTES = 256 * 1024;
export const MAX_WORKFLOW_FILES = 1_000;
@@ -71,6 +71,30 @@ export const RULES = Object.freeze({
description: "Retries need idempotency, bounded attempts, and backoff.",
remediation: "Add a deduplication guard, keep maxTries between 2 and 5, and wait at least 100 ms.",
},
+ VF010: {
+ name: "uncontracted-critical-outcome",
+ severity: "error",
+ description: "Money and privileged actions need an explicit, structurally verified outcome contract.",
+ remediation: "Define an outcomeContracts entry with approval, audit, idempotency, limits, failure notification, and recovery evidence.",
+ },
+ VF011: {
+ name: "uncontracted-external-outcome",
+ severity: "warning",
+ description: "Customer communications and destructive data writes need an explicit outcome contract.",
+ remediation: "Define an outcomeContracts entry that identifies the impact and its audit, notification, and recovery nodes.",
+ },
+ VF012: {
+ name: "silent-error-path",
+ severity: "warning",
+ description: "A connected error path should notify or escalate to an operator.",
+ remediation: "Route the error output to a recognized alert, incident, ticket, or human escalation node.",
+ },
+ VF013: {
+ name: "missing-recovery-contract",
+ severity: "warning",
+ description: "High-impact writes need a declared compensation, rollback, or replay path.",
+ remediation: "Declare a recovery strategy and reference the workflow node that implements it.",
+ },
});
const DEFAULT_CONFIG = Object.freeze({
@@ -79,6 +103,14 @@ const DEFAULT_CONFIG = Object.freeze({
killSwitch: ["agent-off", "kill switch", "agent enabled", "ai enabled", "agent status", "pause ai"],
humanHandoff: ["human handoff", "handoff", "human review", "escalate", "manual review", "chatwoot", "ticket"],
idempotency: ["idempotency", "idempotent", "dedupe", "deduplicate", "duplicate", "event ledger", "event id"],
+ approval: ["approval", "approved", "authorize", "authorized", "human review"],
+ audit: ["audit", "ledger", "journal", "event log", "history"],
+ failureNotification: ["error alert", "failure alert", "notify failure", "incident", "pager", "escalate error"],
+ outcomeMoney: ["refund", "payment", "payout", "charge", "transfer funds", "withdraw"],
+ outcomeCustomer: ["customer message", "customer notification", "notify customer", "client message", "patient message"],
+ outcomePrivileged: ["delete account", "revoke access", "grant access", "change role", "disable user", "publish"],
+ outcomeData: ["delete record", "drop table", "truncate", "purge", "overwrite data"],
+ recovery: ["compensate", "rollback", "replay", "reverse", "restore", "recovery"],
},
bannedNodeTypes: [
"n8n-nodes-base.executecommand",
@@ -86,6 +118,7 @@ const DEFAULT_CONFIG = Object.freeze({
"n8n-nodes-base.readwritefile",
"n8n-nodes-base.localfiletrigger",
],
+ outcomeContracts: {},
});
const SIDE_EFFECT_SUFFIXES = [
@@ -150,6 +183,34 @@ const HANDOFF_SUFFIXES = [
".jira",
];
+const DURABLE_AUDIT_SUFFIXES = [
+ ".postgres",
+ ".mysql",
+ ".microsoftsql",
+ ".supabase",
+ ".datatable",
+ ".mongodb",
+ ".dynamodb",
+ ".s3",
+ ".airtable",
+];
+
+const COMMUNICATION_SUFFIXES = [
+ ".httprequest",
+ ".slack",
+ ".gmail",
+ ".emailsend",
+ ".telegram",
+ ".twilio",
+ ".microsoftteams",
+ ".discord",
+ ".zendesk",
+ ".freshdesk",
+ ".intercom",
+ ".servicenow",
+ ".jira",
+];
+
const TRIGGER_SUFFIXES = [".webhook", ".formtrigger", ".chattrigger", ".telegramtrigger", ".stripetrigger"];
const SAFE_LITERAL = /^(?:<[^>]+>|redacted|change[-_ ]?me|your[-_ ].*|example(?:[-_ ].*)?|placeholder(?:[-_ ].*)?)$/i;
@@ -183,7 +244,7 @@ export function normalizeConfig(input = {}, { locked = false } = {}) {
if (!input || typeof input !== "object" || Array.isArray(input)) {
throw new Error("Vibeflow config must be a JSON object");
}
- const allowedKeys = new Set(["$schema", "rules", "terms", "bannedNodeTypes"]);
+ const allowedKeys = new Set(["$schema", "rules", "terms", "bannedNodeTypes", "outcomeContracts"]);
const unknownKey = Object.keys(input).find((key) => !allowedKeys.has(key));
if (unknownKey) throw new Error(`Unknown config key: ${unknownKey}`);
const config = cloneDefaults();
@@ -232,6 +293,10 @@ export function normalizeConfig(input = {}, { locked = false } = {}) {
config.bannedNodeTypes = normalizedTypes;
}
+ if (input.outcomeContracts !== undefined) {
+ config.outcomeContracts = normalizeOutcomeContracts(input.outcomeContracts);
+ }
+
return config;
}
@@ -278,6 +343,7 @@ export function inspectWorkflow(workflow, config = cloneDefaults()) {
if (shapeError) return [invalidFinding(shapeError)];
const nodes = workflow.nodes;
const { adjacency, reverse } = buildGraph(workflow.connections ?? {});
+ const nodesByName = new Map(nodes.map((node) => [node.name, node]));
const aiNodes = nodes.filter(isAiAgent);
const triggerNodes = nodes.filter(isInboundTrigger);
const sideEffectNodes = nodes.filter(isSideEffect);
@@ -288,13 +354,14 @@ export function inspectWorkflow(workflow, config = cloneDefaults()) {
.map((node) => node.name));
const handoffNames = nodes.filter((node) => isHumanHandoff(node, config.terms.humanHandoff)).map((node) => node.name);
const idempotencyNames = new Set(nodes.filter((node) => isAtomicIdempotencyGuard(node, config.terms.idempotency)).map((node) => node.name));
+ const failureNotificationNames = new Set(nodes
+ .filter((node) => isFailureNotification(node, config.terms.failureNotification))
+ .map((node) => node.name));
const allReachable = traverseGraph(adjacency, entryNames);
const reachableWithoutKillSwitch = traverseGraph(adjacency, entryNames, killSwitchNames);
const canReachHandoff = traverseGraph(reverse, handoffNames);
const triggerNames = triggerNodes.map((node) => node.name);
const triggerReachable = traverseGraph(adjacency, triggerNames);
- const triggerReachableWithoutIdempotency = traverseGraph(adjacency, triggerNames, idempotencyNames);
- const entryReachableWithoutIdempotency = traverseGraph(adjacency, entryNames, idempotencyNames);
const workflowHasErrorHandler = typeof workflow.settings?.errorWorkflow === "string" && workflow.settings.errorWorkflow.trim();
const findings = [];
let findingsTruncated = false;
@@ -339,10 +406,19 @@ export function inspectWorkflow(workflow, config = cloneDefaults()) {
add("VF004", "External-action node has no connected error output or workflow error handler", node);
}
+ if (isSideEffect(node)
+ && !failureNotificationNames.has(node.name)
+ && !workflowHasErrorHandler
+ && hasConnectedErrorPath(node, workflow.connections ?? {})
+ && !errorBranchCanReach(node.name, failureNotificationNames, workflow.connections ?? {}, adjacency)) {
+ add("VF012", "Connected error output terminates without a recognized operator notification or escalation", node);
+ }
+
if (isSideEffect(node) && node.retryOnFail === true) {
const maxTries = Number(node.maxTries ?? 3);
const waitBetweenTries = Number(node.waitBetweenTries ?? 0);
- if (!idempotencyNames.has(node.name) && (!allReachable.has(node.name) || entryReachableWithoutIdempotency.has(node.name))) {
+ if (!idempotencyNames.has(node.name)
+ && !hasProtectedIdempotencyPath(node.name, entryNames, idempotencyNames, adjacency, workflow.connections ?? {})) {
add("VF009", "Retry is enabled on a path without an atomic idempotency guard", node);
}
if (!Number.isFinite(maxTries) || maxTries < 2 || maxTries > 5) {
@@ -374,13 +450,27 @@ export function inspectWorkflow(workflow, config = cloneDefaults()) {
}
for (const sideEffectNode of sideEffectNodes) {
- if (!idempotencyNames.has(sideEffectNode.name)
+ if (!failureNotificationNames.has(sideEffectNode.name)
+ && !idempotencyNames.has(sideEffectNode.name)
&& triggerReachable.has(sideEffectNode.name)
- && triggerReachableWithoutIdempotency.has(sideEffectNode.name)) {
+ && !hasProtectedIdempotencyPath(sideEffectNode.name, triggerNames, idempotencyNames, adjacency, workflow.connections ?? {})) {
add("VF005", "Inbound path reaches this external side effect without an atomic idempotency guard", sideEffectNode);
}
}
+ inspectOutcomeContracts({
+ nodes,
+ nodesByName,
+ config,
+ adjacency,
+ connections: workflow.connections ?? {},
+ entryNames,
+ allReachable,
+ idempotencyNames,
+ failureNotificationNames,
+ add,
+ });
+
for (const aiNode of aiNodes) {
if (!allReachable.has(aiNode.name) || reachableWithoutKillSwitch.has(aiNode.name)) {
add("VF006", "At least one entry path reaches the AI Agent without a structural kill-switch gate", aiNode);
@@ -462,9 +552,82 @@ function cloneDefaults() {
rules: { ...DEFAULT_CONFIG.rules },
terms: Object.fromEntries(Object.entries(DEFAULT_CONFIG.terms).map(([key, values]) => [key, [...values]])),
bannedNodeTypes: [...DEFAULT_CONFIG.bannedNodeTypes],
+ outcomeContracts: {},
};
}
+function normalizeOutcomeContracts(input) {
+ if (!input || typeof input !== "object" || Array.isArray(input)) {
+ throw new Error("config.outcomeContracts must be an object keyed by exact node name");
+ }
+ const entries = Object.entries(input);
+ if (entries.length > 1_000) throw new Error("config.outcomeContracts must contain at most 1000 entries");
+ const normalizedEntries = [];
+ for (const [nodeName, value] of entries) {
+ if (!nodeName.trim() || nodeName.length > 200) {
+ throw new Error("Outcome contract node names must contain 1 to 200 characters");
+ }
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
+ throw new Error(`Outcome contract for ${safeDisplay(nodeName)} must be an object`);
+ }
+ const allowedKeys = new Set([
+ "impact", "approvalNode", "auditNode", "failureNotificationNode",
+ "amountGuard", "counterpartyGuard", "recovery",
+ ]);
+ const unknownKey = Object.keys(value).find((key) => !allowedKeys.has(key));
+ if (unknownKey) throw new Error(`Unknown outcome contract key for ${safeDisplay(nodeName)}: ${safeDisplay(unknownKey)}`);
+ if (!["money", "customer", "privileged", "data"].includes(value.impact)) {
+ throw new Error(`Outcome contract for ${safeDisplay(nodeName)} must declare impact as money, customer, privileged, or data`);
+ }
+ for (const field of ["approvalNode", "auditNode", "failureNotificationNode"]) {
+ if (value[field] !== undefined) validateNodeReference(value[field], `${nodeName}.${field}`);
+ }
+ if (value.amountGuard !== undefined) {
+ validateNestedContract(value.amountGuard, `${nodeName}.amountGuard`, ["node", "maximum", "currency"]);
+ validateNodeReference(value.amountGuard.node, `${nodeName}.amountGuard.node`);
+ if (typeof value.amountGuard.maximum !== "number" || !Number.isFinite(value.amountGuard.maximum) || value.amountGuard.maximum <= 0) {
+ throw new Error(`Outcome contract ${safeDisplay(nodeName)}.amountGuard.maximum must be a positive finite number`);
+ }
+ if (typeof value.amountGuard.currency !== "string" || !/^[A-Z]{3}$/.test(value.amountGuard.currency)) {
+ throw new Error(`Outcome contract ${safeDisplay(nodeName)}.amountGuard.currency must be a three-letter uppercase currency code`);
+ }
+ }
+ if (value.counterpartyGuard !== undefined) {
+ validateNestedContract(value.counterpartyGuard, `${nodeName}.counterpartyGuard`, ["node", "allowed"]);
+ validateNodeReference(value.counterpartyGuard.node, `${nodeName}.counterpartyGuard.node`);
+ if (!Array.isArray(value.counterpartyGuard.allowed)
+ || !value.counterpartyGuard.allowed.length
+ || value.counterpartyGuard.allowed.length > 100
+ || value.counterpartyGuard.allowed.some((item) => typeof item !== "string" || !item.trim() || item.length > 100)) {
+ throw new Error(`Outcome contract ${safeDisplay(nodeName)}.counterpartyGuard.allowed must contain 1 to 100 non-empty strings`);
+ }
+ }
+ if (value.recovery !== undefined) {
+ validateNestedContract(value.recovery, `${nodeName}.recovery`, ["strategy", "node"]);
+ validateNodeReference(value.recovery.node, `${nodeName}.recovery.node`);
+ if (!["compensate", "rollback", "replay"].includes(value.recovery.strategy)) {
+ throw new Error(`Outcome contract ${safeDisplay(nodeName)}.recovery.strategy must be compensate, rollback, or replay`);
+ }
+ }
+ normalizedEntries.push([nodeName, structuredClone(value)]);
+ }
+ return Object.fromEntries(normalizedEntries);
+}
+
+function validateNestedContract(value, pathName, allowedKeys) {
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
+ throw new Error(`Outcome contract ${safeDisplay(pathName)} must be an object`);
+ }
+ const unknownKey = Object.keys(value).find((key) => !allowedKeys.includes(key));
+ if (unknownKey) throw new Error(`Unknown outcome contract key ${safeDisplay(pathName)}.${safeDisplay(unknownKey)}`);
+}
+
+function validateNodeReference(value, pathName) {
+ if (typeof value !== "string" || !value.trim() || value.length > 200) {
+ throw new Error(`Outcome contract ${safeDisplay(pathName)} must be a non-empty node name of at most 200 characters`);
+ }
+}
+
async function collectWorkflowFiles(inputs) {
const files = new Set();
for (const input of inputs) {
@@ -697,6 +860,237 @@ function hasConnectedErrorPath(node, connections) {
&& output.some((connection) => reachesTerminalWithout(connection.node, node.name, connections)));
}
+function inspectOutcomeContracts({
+ nodes,
+ nodesByName,
+ config,
+ adjacency,
+ connections,
+ entryNames,
+ allReachable,
+ idempotencyNames,
+ failureNotificationNames,
+ add,
+}) {
+ const contracts = config.outcomeContracts ?? {};
+ const evidenceNodeNames = new Set(Object.values(contracts).flatMap((contract) => [
+ contract.auditNode,
+ contract.failureNotificationNode,
+ contract.recovery?.node,
+ ]).filter(Boolean));
+ const inspected = new Set();
+
+ for (const node of nodes) {
+ const contract = contracts[node.name];
+ const impact = contract?.impact ?? (evidenceNodeNames.has(node.name) ? null : classifyOutcome(node, config.terms));
+ if (!impact) continue;
+ inspected.add(node.name);
+ const ruleId = ["money", "privileged"].includes(impact) ? "VF010" : "VF011";
+ if (!contract) {
+ add(ruleId, `Detected ${impact} outcome has no outcomeContracts entry`, node);
+ add("VF013", `Detected ${impact} outcome has no declared recovery strategy`, node);
+ continue;
+ }
+
+ const issues = [];
+ const recoveryIssues = [];
+ const auditNode = nodesByName.get(contract.auditNode);
+ if (!auditNode) {
+ issues.push("missing auditNode reference");
+ } else if (!isDurableAuditNode(auditNode, config.terms.audit)) {
+ issues.push(`auditNode ${contract.auditNode} is not recognized as a durable audit write`);
+ } else if (!dominatesTarget(contract.auditNode, node.name, adjacency, entryNames, allReachable)) {
+ issues.push(`auditNode ${contract.auditNode} does not guard every entry path to the action`);
+ } else if (errorBranchCanReach(contract.auditNode, new Set([node.name]), connections, adjacency)) {
+ issues.push(`auditNode ${contract.auditNode} can reach the action after an audit failure`);
+ }
+
+ const notificationNode = nodesByName.get(contract.failureNotificationNode);
+ if (!notificationNode || contract.failureNotificationNode === node.name) {
+ issues.push("missing failureNotificationNode reference");
+ } else if (!failureNotificationNames.has(contract.failureNotificationNode)) {
+ issues.push(`failureNotificationNode ${contract.failureNotificationNode} is not recognized as an operator alert`);
+ } else if (!errorBranchCanReach(node.name, new Set([contract.failureNotificationNode]), connections, adjacency)) {
+ issues.push(`failureNotificationNode ${contract.failureNotificationNode} is not reachable from the action error output`);
+ }
+
+ if (!hasProtectedIdempotencyPath(node.name, entryNames, idempotencyNames, adjacency, connections)) {
+ issues.push("an entry path reaches the action without an atomic idempotency claim");
+ }
+
+ if (["money", "privileged"].includes(impact)) {
+ const approvalNode = nodesByName.get(contract.approvalNode);
+ if (!approvalNode) {
+ issues.push("missing approvalNode reference");
+ } else if (!isStructuralGuard(approvalNode, node.name, adjacency, connections, entryNames, allReachable)
+ || !lowerType(approvalNode).endsWith(".if")
+ || !hasPositiveDynamicBooleanCondition(approvalNode.parameters ?? {}, config.terms.approval)) {
+ issues.push(`approvalNode ${contract.approvalNode} is not a dominating approval gate with a deny branch`);
+ }
+ }
+
+ if (impact === "money") {
+ const amountNode = nodesByName.get(contract.amountGuard?.node);
+ if (!amountNode) {
+ issues.push("missing amountGuard node reference");
+ } else if (!isStructuralGuard(amountNode, node.name, adjacency, connections, entryNames, allReachable)
+ || !hasDynamicUpperBound(amountNode.parameters ?? {}, contract.amountGuard.maximum)
+ || !nodeContainsExactValue(amountNode.parameters ?? {}, contract.amountGuard.currency)) {
+ issues.push(`amountGuard ${contract.amountGuard.node} does not structurally enforce the declared maximum and currency`);
+ }
+
+ const counterpartyNode = nodesByName.get(contract.counterpartyGuard?.node);
+ if (!counterpartyNode) {
+ issues.push("missing counterpartyGuard node reference");
+ } else if (!isStructuralGuard(counterpartyNode, node.name, adjacency, connections, entryNames, allReachable)
+ || !valueContainsDirectDynamicReference(counterpartyNode.parameters ?? {})
+ || !contract.counterpartyGuard.allowed.every((value) => nodeContainsExactValue(counterpartyNode.parameters ?? {}, value))) {
+ issues.push(`counterpartyGuard ${contract.counterpartyGuard.node} does not structurally enforce every allowed counterparty`);
+ }
+ }
+
+ const recoveryNode = nodesByName.get(contract.recovery?.node);
+ if (!recoveryNode || contract.recovery?.node === node.name) {
+ recoveryIssues.push("missing recovery node reference");
+ } else if (!nodeContainsTerms(recoveryNode, config.terms.recovery)) {
+ recoveryIssues.push(`recovery node ${contract.recovery.node} is not recognized as compensation, rollback, or replay logic`);
+ } else if (["compensate", "rollback"].includes(contract.recovery.strategy)
+ && !traverseGraph(adjacency, [node.name]).has(contract.recovery.node)) {
+ recoveryIssues.push(`${contract.recovery.strategy} node ${contract.recovery.node} is not downstream from the action`);
+ }
+
+ if (issues.length) add(ruleId, `Outcome contract is incomplete: ${issues.join("; ")}`, node);
+ if (recoveryIssues.length) add("VF013", `Recovery contract is incomplete: ${recoveryIssues.join("; ")}`, node);
+ }
+
+ for (const [nodeName, contract] of Object.entries(contracts)) {
+ if (inspected.has(nodeName) || nodesByName.has(nodeName)) continue;
+ const ruleId = ["money", "privileged"].includes(contract.impact) ? "VF010" : "VF011";
+ add(ruleId, `Outcome contract references missing action node: ${nodeName}`);
+ }
+}
+
+function classifyOutcome(node, terms) {
+ if (!isOutcomeAction(node)) return null;
+ if (nodeContainsTerms(node, terms.outcomeMoney)) return "money";
+ if (nodeContainsTerms(node, terms.outcomePrivileged)) return "privileged";
+ if (isDestructiveDataAction(node) || nodeContainsTerms(node, terms.outcomeData)) return "data";
+ if (isCommunicationNode(node) && nodeContainsTerms(node, terms.outcomeCustomer)) return "customer";
+ return null;
+}
+
+function isCommunicationNode(node) {
+ return COMMUNICATION_SUFFIXES.some((suffix) => lowerType(node).endsWith(suffix));
+}
+
+function isOutcomeAction(node) {
+ if (!isSideEffect(node)) return false;
+ if (!lowerType(node).endsWith(".httprequest")) return true;
+ return ["POST", "PUT", "PATCH", "DELETE"].includes(String(node.parameters?.method ?? "GET").toUpperCase());
+}
+
+function isDestructiveDataAction(node) {
+ if (!DURABLE_AUDIT_SUFFIXES.some((suffix) => lowerType(node).endsWith(suffix))) return false;
+ const operation = String(node.parameters?.operation ?? node.parameters?.resourceOperation ?? "").toLowerCase();
+ if (["delete", "remove", "truncate", "drop"].includes(operation)) return true;
+ return nodeContainsSqlStatement(node.parameters ?? {}, /^\s*(?:delete\s+from|drop\s+(?:table|schema|database)|truncate\b)/i);
+}
+
+function isFailureNotification(node, terms) {
+ if (!isCommunicationNode(node) || !nodeContainsTerms(node, terms)) return false;
+ if (!lowerType(node).endsWith(".httprequest")) return true;
+ return ["POST", "PUT", "PATCH"].includes(String(node.parameters?.method ?? "GET").toUpperCase());
+}
+
+function errorBranchCanReach(nodeName, targetNames, connections, adjacency) {
+ const branches = connections[nodeName]?.main;
+ if (!Array.isArray(branches)) return false;
+ const starts = branches.slice(1).flatMap((branch) => (branch ?? []))
+ .filter((connection) => connection.type === "main")
+ .map((connection) => connection.node);
+ if (!starts.length) return false;
+ const reachable = traverseGraph(adjacency, starts);
+ return [...targetNames].some((target) => reachable.has(target));
+}
+
+function isDurableAuditNode(node, terms) {
+ if (!DURABLE_AUDIT_SUFFIXES.some((suffix) => lowerType(node).endsWith(suffix))
+ || !nodeContainsTerms(node, terms)
+ || node.continueOnFail === true
+ || node.onError === "continueRegularOutput") return false;
+ const operation = String(node.parameters?.operation ?? node.parameters?.resourceOperation ?? "").toLowerCase();
+ if (["create", "update", "append", "insert", "upsert", "add", "put"].includes(operation)) return true;
+ if (operation && operation !== "executequery") return false;
+ return nodeContainsSqlStatement(node.parameters ?? {}, /^\s*(?:insert|update|merge)\b/i);
+}
+
+function dominatesTarget(blockerName, targetName, adjacency, entryNames, allReachable) {
+ if (blockerName === targetName || !allReachable.has(targetName)) return false;
+ return !traverseGraph(adjacency, entryNames, new Set([blockerName])).has(targetName);
+}
+
+function isStructuralGuard(node, targetName, adjacency, connections, entryNames, allReachable) {
+ if (![".if", ".switch"].some((suffix) => lowerType(node).endsWith(suffix))) return false;
+ if (!dominatesTarget(node.name, targetName, adjacency, entryNames, allReachable)) return false;
+ const branches = connections[node.name]?.main;
+ if (!Array.isArray(branches) || branches.length < 2) return false;
+ const reachesTarget = branches.map((branch) => {
+ const starts = (branch ?? []).filter((connection) => connection.type === "main").map((connection) => connection.node);
+ return starts.length > 0 && traverseGraph(adjacency, starts).has(targetName);
+ });
+ return reachesTarget.some(Boolean) && reachesTarget.some((value) => !value);
+}
+
+function nodeContainsExactValue(input, expected) {
+ const normalizedExpected = typeof expected === "string" ? expected.trim().toLowerCase() : expected;
+ const stack = [input];
+ while (stack.length) {
+ const value = stack.pop();
+ if (typeof value === "string" && typeof normalizedExpected === "string" && value.trim().toLowerCase() === normalizedExpected) return true;
+ if (typeof value === "number" && typeof normalizedExpected === "number" && value === normalizedExpected) return true;
+ if (Array.isArray(value)) {
+ for (const child of value) stack.push(child);
+ } else if (value && typeof value === "object") {
+ for (const child of Object.values(value)) stack.push(child);
+ }
+ }
+ return false;
+}
+
+function hasDynamicUpperBound(input, maximum) {
+ const stack = [input];
+ const allowedOperations = new Set(["smaller", "smallerequal", "lessthan", "lessthanorequal", "lte"]);
+ while (stack.length) {
+ const value = stack.pop();
+ if (Array.isArray(value)) {
+ for (const child of value) stack.push(child);
+ continue;
+ }
+ if (!value || typeof value !== "object") continue;
+ const values = Object.values(value);
+ const operation = String(value.operation ?? "").toLowerCase().replace(/[^a-z]/g, "");
+ if (allowedOperations.has(operation)
+ && values.some((child) => typeof child === "string" && isDirectDynamicReference(child))
+ && values.some((child) => typeof child === "number" && child === maximum)) return true;
+ for (const child of values) stack.push(child);
+ }
+ return false;
+}
+
+function valueContainsDirectDynamicReference(input) {
+ const stack = [input];
+ while (stack.length) {
+ const value = stack.pop();
+ if (typeof value === "string" && isDirectDynamicReference(value)) return true;
+ if (Array.isArray(value)) {
+ for (const child of value) stack.push(child);
+ } else if (value && typeof value === "object") {
+ for (const child of Object.values(value)) stack.push(child);
+ }
+ }
+ return false;
+}
+
function reachesTerminalWithout(start, excluded, connections) {
const stack = [start];
const seen = new Set();
@@ -762,13 +1156,20 @@ function isHumanHandoff(node, terms) {
}
function isAtomicIdempotencyGuard(node, terms) {
- if (!nodeContainsTerms(node, terms)) return false;
+ if (!nodeContainsTerms(node, terms) || node.continueOnFail === true || node.onError === "continueRegularOutput") return false;
const type = lowerType(node);
const parameters = node.parameters ?? {};
return type.endsWith(".postgres")
&& nodeContainsSqlStatement(parameters, /^\s*insert\b[\s\S]*\bon\s+conflict\b[\s\S]*\bdo\s+nothing\b[\s\S]*\breturning\b/i);
}
+function hasProtectedIdempotencyPath(targetName, starts, idempotencyNames, adjacency, connections) {
+ const validGuards = new Set([...idempotencyNames].filter((guardName) =>
+ !errorBranchCanReach(guardName, new Set([targetName]), connections, adjacency)));
+ if (!validGuards.size || !traverseGraph(adjacency, starts).has(targetName)) return false;
+ return !traverseGraph(adjacency, starts, validGuards).has(targetName);
+}
+
function nodeContainsSqlStatement(value, pattern) {
let matches = false;
walk({ value }, "", (child) => {
diff --git a/test/vibeflow.test.mjs b/test/vibeflow.test.mjs
index 6cac8b5..2fa1379 100644
--- a/test/vibeflow.test.mjs
+++ b/test/vibeflow.test.mjs
@@ -89,7 +89,7 @@ test("plugin and skill manifests contain no scaffold placeholders", async () =>
const plugin = JSON.parse(await readFile(pluginPath, "utf8"));
const skill = await readFile(skillPath, "utf8");
assert.equal(plugin.name, "vibeflow");
- assert.equal(plugin.version, "0.8.0");
+ assert.equal(plugin.version, "0.9.0");
assert.match(skill, /^---\nname: vibeflow\ndescription: .+\n---/);
assert.doesNotMatch(`${JSON.stringify(plugin)}\n${skill}`, /\[TODO:/);
});
@@ -176,6 +176,20 @@ test("idempotency must be atomic and dominate inbound side-effect paths", () =>
Webhook: { main: [[connection(guard.name)]] },
[guard.name]: { main: [[connection("Send")]] },
}), "VF005", "Send"), true);
+
+ guard.parameters.query = "INSERT INTO event_ledger VALUES ($1) ON CONFLICT DO NOTHING RETURNING event_id";
+ guard.continueOnFail = true;
+ assert.equal(findingFor(workflowWith([nodes[0], guard, nodes[1]], {
+ Webhook: { main: [[connection(guard.name)]] },
+ [guard.name]: { main: [[connection("Send")]] },
+ }), "VF005", "Send"), true);
+
+ guard.continueOnFail = false;
+ guard.onError = "continueErrorOutput";
+ assert.equal(findingFor(workflowWith([nodes[0], guard, nodes[1]], {
+ Webhook: { main: [[connection(guard.name)]] },
+ [guard.name]: { main: [[connection("Send")], [connection("Send")]] },
+ }), "VF005", "Send"), true);
});
test("kill switches must be structural and dominate every AI entry path", () => {
@@ -303,6 +317,175 @@ test("error handling must be connected or configured at workflow level", () => {
assert.equal(findingFor(indirectLoop, "VF004", request.name), true);
});
+test("ordinary HTTP money actions require a critical outcome contract", () => {
+ const refund = {
+ name: "Issue refund",
+ type: "n8n-nodes-base.httpRequest",
+ parameters: { method: "POST", url: "https://payments.invalid/refund" },
+ };
+ const workflow = workflowWith([refund], {}, { executionTimeout: 120, errorWorkflow: "global-errors" });
+ assert.equal(findingFor(workflow, "VF010", refund.name), true);
+ assert.equal(findingFor(workflow, "VF013", refund.name), true);
+
+ refund.parameters.method = "GET";
+ assert.equal(findingFor(workflow, "VF010", refund.name), false);
+});
+
+test("complete money outcome contract passes VF010 through VF013", () => {
+ const { workflow, config } = moneyOutcomeFixture();
+ const findings = inspectWorkflow(workflow, config);
+ for (const ruleId of ["VF010", "VF011", "VF012", "VF013"]) {
+ assert.equal(findings.some((finding) => finding.ruleId === ruleId), false, ruleId);
+ }
+});
+
+test("incomplete money contracts report missing structural policy evidence", () => {
+ const { workflow } = moneyOutcomeFixture();
+ workflow.connections.Webhook = { main: [[connection("Record refund audit")]] };
+ const config = normalizeConfig({ outcomeContracts: { "Issue refund": { impact: "money" } } });
+ const findings = inspectWorkflow(workflow, config);
+ const critical = findings.find((finding) => finding.ruleId === "VF010" && finding.node?.name === "Issue refund");
+ assert.match(critical.message, /auditNode/);
+ assert.match(critical.message, /idempotency/);
+ assert.match(critical.message, /approvalNode/);
+ assert.match(critical.message, /amountGuard/);
+ assert.match(critical.message, /counterpartyGuard/);
+ assert.equal(findingForWithConfig(workflow, config, "VF013", "Issue refund"), true);
+});
+
+test("customer communications and destructive writes require external outcome contracts", () => {
+ const customerMessage = {
+ name: "Notify customer",
+ type: "n8n-nodes-base.httpRequest",
+ parameters: { method: "POST", url: "https://messaging.invalid/customer-message" },
+ };
+ const destructiveWrite = {
+ name: "Execute maintenance query",
+ type: "n8n-nodes-base.postgres",
+ parameters: { operation: "executeQuery", query: "DELETE FROM records WHERE id = $1" },
+ };
+ const workflow = workflowWith([customerMessage, destructiveWrite], {}, {
+ executionTimeout: 120,
+ errorWorkflow: "global-errors",
+ });
+ assert.equal(findingFor(workflow, "VF011", customerMessage.name), true);
+ assert.equal(findingFor(workflow, "VF011", destructiveWrite.name), true);
+});
+
+test("audit-like labels cannot hide an uncontracted money action", () => {
+ const paymentHistory = {
+ name: "Update payment history",
+ type: "n8n-nodes-base.postgres",
+ parameters: { operation: "update", table: "payment_history" },
+ };
+ const workflow = workflowWith([paymentHistory], {}, { executionTimeout: 120, errorWorkflow: "global-errors" });
+ assert.equal(findingFor(workflow, "VF010", paymentHistory.name), true);
+});
+
+test("connected error paths must reach a recognized operator notification", () => {
+ const request = {
+ name: "Create external record",
+ type: "n8n-nodes-base.httpRequest",
+ onError: "continueErrorOutput",
+ parameters: { method: "POST" },
+ };
+ const stop = { name: "Failure stop", type: "n8n-nodes-base.noOp", parameters: {} };
+ const silent = workflowWith([request, stop], {
+ [request.name]: { main: [[], [connection(stop.name)]] },
+ });
+ assert.equal(findingFor(silent, "VF012", request.name), true);
+
+ const alert = {
+ name: "Operator error alert",
+ type: "n8n-nodes-base.slack",
+ onError: "continueErrorOutput",
+ parameters: { channel: "operations", text: "Failure alert" },
+ };
+ const notified = workflowWith([request, alert, stop], {
+ [request.name]: { main: [[], [connection(alert.name)]] },
+ [alert.name]: { main: [[], [connection(stop.name)]] },
+ });
+ assert.equal(findingFor(notified, "VF012", request.name), false);
+});
+
+test("named but disconnected policy nodes cannot satisfy an outcome contract", () => {
+ const { workflow, config } = moneyOutcomeFixture();
+ workflow.connections.Webhook = { main: [[connection("Issue refund")]] };
+ assert.equal(findingForWithConfig(workflow, config, "VF010", "Issue refund"), true);
+});
+
+test("constant approvals and read-only audit labels do not satisfy outcome contracts", () => {
+ const constantApproval = moneyOutcomeFixture();
+ constantApproval.workflow.nodes.find((node) => node.name === "Approve refund").parameters = {
+ conditions: { boolean: [{ value1: true, value2: true }] },
+ note: "approved",
+ };
+ assert.equal(findingForWithConfig(constantApproval.workflow, constantApproval.config, "VF010", "Issue refund"), true);
+
+ const readOnlyAudit = moneyOutcomeFixture();
+ readOnlyAudit.workflow.nodes.find((node) => node.name === "Record refund audit").parameters = {
+ operation: "executeQuery",
+ query: "SELECT * FROM refund_audit",
+ };
+ assert.equal(findingForWithConfig(readOnlyAudit.workflow, readOnlyAudit.config, "VF010", "Issue refund"), true);
+
+ const decorativeAmount = moneyOutcomeFixture();
+ decorativeAmount.workflow.nodes.find((node) => node.name === "Limit refund amount").parameters = {
+ conditions: { boolean: [{ value1: true, value2: true }] },
+ note: "={{ $json.amount }}",
+ maximum: 500,
+ currency: "USD",
+ };
+ assert.equal(findingForWithConfig(decorativeAmount.workflow, decorativeAmount.config, "VF010", "Issue refund"), true);
+
+ const failOpenAudit = moneyOutcomeFixture();
+ const auditNode = failOpenAudit.workflow.nodes.find((node) => node.name === "Record refund audit");
+ auditNode.onError = "continueErrorOutput";
+ failOpenAudit.workflow.connections["Record refund audit"] = {
+ main: [[connection("Approve refund")], [connection("Issue refund")]],
+ };
+ assert.equal(findingForWithConfig(failOpenAudit.workflow, failOpenAudit.config, "VF010", "Issue refund"), true);
+});
+
+test("outcome contracts resist prototype keys and self-referential evidence", () => {
+ const prototypeConfig = normalizeConfig(JSON.parse(`{
+ "outcomeContracts": {
+ "__proto__": { "impact": "customer" }
+ }
+ }`));
+ assert.equal(Object.hasOwn(prototypeConfig.outcomeContracts, "__proto__"), true);
+ assert.equal(Object.getPrototypeOf(prototypeConfig.outcomeContracts), Object.prototype);
+
+ const { workflow } = moneyOutcomeFixture();
+ const config = normalizeConfig({
+ outcomeContracts: {
+ "Issue refund": {
+ impact: "money",
+ approvalNode: "Approve refund",
+ auditNode: "Record refund audit",
+ failureNotificationNode: "Issue refund",
+ amountGuard: { node: "Limit refund amount", maximum: 500, currency: "USD" },
+ counterpartyGuard: { node: "Allow refund account", allowed: ["merchant-primary"] },
+ recovery: { strategy: "compensate", node: "Issue refund" },
+ },
+ },
+ });
+ const findings = inspectWorkflow(workflow, config);
+ assert.equal(findings.some((finding) => finding.ruleId === "VF010" && /failureNotificationNode/.test(finding.message)), true);
+ assert.equal(findings.some((finding) => finding.ruleId === "VF013"), true);
+});
+
+test("outcome contract configuration rejects ambiguous shapes", () => {
+ assert.throws(() => normalizeConfig({ outcomeContracts: [] }), /must be an object/);
+ assert.throws(() => normalizeConfig({ outcomeContracts: { Refund: { impact: "physical" } } }), /must declare impact/);
+ assert.throws(() => normalizeConfig({
+ outcomeContracts: { Refund: { impact: "money", amountGuard: { node: "Limit", maximum: -1, currency: "usd" } } },
+ }), /positive finite number/);
+ assert.throws(() => normalizeConfig({
+ outcomeContracts: { Refund: { impact: "money", recovery: { node: "Undo", strategy: "hope" } } },
+ }), /must be compensate, rollback, or replay/);
+});
+
test("malformed connection metadata cannot fabricate graph edges", () => {
const workflow = workflowWith([
{ name: "AI Agent", type: "@n8n/n8n-nodes-langchain.agent", parameters: {} },
@@ -459,6 +642,86 @@ function findingFor(workflow, ruleId, nodeName) {
return inspectWorkflow(workflow).some((finding) => finding.ruleId === ruleId && finding.node?.name === nodeName);
}
+function findingForWithConfig(workflow, config, ruleId, nodeName) {
+ return inspectWorkflow(workflow, config).some((finding) => finding.ruleId === ruleId && finding.node?.name === nodeName);
+}
+
+function moneyOutcomeFixture() {
+ const nodes = [
+ {
+ name: "Webhook",
+ type: "n8n-nodes-base.webhook",
+ parameters: { authentication: "headerAuth" },
+ credentials: { httpHeaderAuth: { id: "credential-reference" } },
+ },
+ {
+ name: "Claim idempotency event",
+ type: "n8n-nodes-base.postgres",
+ parameters: { query: "INSERT INTO event_ledger VALUES ($1) ON CONFLICT DO NOTHING RETURNING event_id" },
+ },
+ {
+ name: "Record refund audit",
+ type: "n8n-nodes-base.postgres",
+ parameters: { operation: "insert", table: "refund_audit" },
+ },
+ {
+ name: "Approve refund",
+ type: "n8n-nodes-base.if",
+ parameters: { conditions: { boolean: [{ value1: "={{ $json.approved }}", value2: true }] } },
+ },
+ {
+ name: "Limit refund amount",
+ type: "n8n-nodes-base.if",
+ parameters: { conditions: { number: [{ value1: "={{ $json.amount }}", operation: "smallerEqual", value2: 500 }] }, currency: "USD" },
+ },
+ {
+ name: "Allow refund account",
+ type: "n8n-nodes-base.switch",
+ parameters: { value: "={{ $json.counterparty }}", rules: [{ value: "merchant-primary" }] },
+ },
+ {
+ name: "Issue refund",
+ type: "n8n-nodes-base.httpRequest",
+ onError: "continueErrorOutput",
+ parameters: { method: "POST", url: "https://payments.invalid/refund" },
+ },
+ {
+ name: "Notify refund failure",
+ type: "n8n-nodes-base.slack",
+ parameters: { channel: "operations", text: "Notify failure for refund" },
+ },
+ {
+ name: "Compensate transaction",
+ type: "n8n-nodes-base.httpRequest",
+ parameters: { method: "POST", url: "https://payments.invalid/reverse", operation: "compensate" },
+ },
+ ];
+ const connections = {
+ Webhook: { main: [[connection("Claim idempotency event")]] },
+ "Claim idempotency event": { main: [[connection("Record refund audit")]] },
+ "Record refund audit": { main: [[connection("Approve refund")]] },
+ "Approve refund": { main: [[connection("Limit refund amount")], []] },
+ "Limit refund amount": { main: [[connection("Allow refund account")], []] },
+ "Allow refund account": { main: [[connection("Issue refund")], []] },
+ "Issue refund": { main: [[], [connection("Notify refund failure"), connection("Compensate transaction")]] },
+ };
+ const workflow = workflowWith(nodes, connections, { executionTimeout: 120, errorWorkflow: "global-errors" });
+ const config = normalizeConfig({
+ outcomeContracts: {
+ "Issue refund": {
+ impact: "money",
+ approvalNode: "Approve refund",
+ auditNode: "Record refund audit",
+ failureNotificationNode: "Notify refund failure",
+ amountGuard: { node: "Limit refund amount", maximum: 500, currency: "USD" },
+ counterpartyGuard: { node: "Allow refund account", allowed: ["merchant-primary"] },
+ recovery: { strategy: "compensate", node: "Compensate transaction" },
+ },
+ },
+ });
+ return { workflow, config };
+}
+
function runCli(args) {
return spawnSync(process.execPath, [path.join(root, "bin/vibeflow.mjs"), ...args], {
cwd: root,
From 7552eeca7cfdd56376eab2007988b81a9726fba4 Mon Sep 17 00:00:00 2001
From: Felipe Domingues <53182096+domfelipe@users.noreply.github.com>
Date: Wed, 22 Jul 2026 14:09:33 -0300
Subject: [PATCH 6/9] Add v0.9 community launch and application evidence (#7)
Add ready-to-post community copy, link the public feedback provenance, and align the Codex for OSS pack with the current official form.
---
README.md | 1 +
docs/codex-for-oss-application.md | 12 ++--
docs/community-launch-v0.9.md | 94 +++++++++++++++++++++++++++++++
docs/launch.md | 2 +
4 files changed, 105 insertions(+), 4 deletions(-)
create mode 100644 docs/community-launch-v0.9.md
diff --git a/README.md b/README.md
index 3f44f04..729bfa0 100644
--- a/README.md
+++ b/README.md
@@ -139,6 +139,7 @@ An outcome contract is structural evidence, not runtime enforcement. Money and c
- [Architecture and limitations](docs/architecture.md)
- [Reproducible demo](docs/demo.md)
- [Release audit](docs/release-audit.md)
+- [Community launch pack](docs/community-launch-v0.9.md)
- [Roadmap](docs/roadmap.md)
- [Codex for Open Source application gate](docs/codex-for-oss-application.md)
- [Contributing](CONTRIBUTING.md)
diff --git a/docs/codex-for-oss-application.md b/docs/codex-for-oss-application.md
index 75710a0..edfc161 100644
--- a/docs/codex-for-oss-application.md
+++ b/docs/codex-for-oss-application.md
@@ -8,6 +8,8 @@ Program terms:
Prepared: 2026-07-22
+Official form rechecked: 2026-07-22
+
## Recommendation
Submit in **English**, even through the PT-BR form. OpenAI publishes no language requirement and no evidence that language changes selection odds. English is recommended only to reduce translation friction for a global technical review.
@@ -38,11 +40,11 @@ Submit immediately after the v0.9.0 release on 2026-07-24. Do not wait for arbit
### Maintainer role
-Select: **Principal maintainer**.
+Select: **Primary maintainer**.
### Why is this repository eligible?
-Recount before submission; keep under 500 characters.
+Character count: **356/500**.
> Vibeflow is an MIT-licensed safety gate for AI-generated n8n workflows, rebuilt end-to-end with Codex. It audited a real 92-node workflow in read-only mode, finding 3 blocking risks and 57 warnings. Community feedback then shaped v0.9: outcome-aware checks for refunds, customer actions, silent failures, audit, idempotency, approval, limits, and recovery.
@@ -67,7 +69,7 @@ Character count: **390/500**.
### Anything else we should know?
-Recount before submission; keep under 500 characters.
+Character count: **381/500**.
> Vibeflow is a public case study in Codex-native OSS development. Codex drove the project from product repositioning through implementation, adversarial review, remediation, CI, plugin packaging, releases, a real-workflow audit, and the v0.9 response to user feedback. Adoption is early, but the engineering, maintenance history, and real-world evidence are public and reproducible.
@@ -97,6 +99,7 @@ Use these links only if OpenAI requests verification; the form has no dedicated
- Release and Red Team audit:
- CI history:
- Launch discussion:
+- Public r/n8n feedback that shaped v0.9:
- Engineering PRs:
## Evidence snapshot
@@ -107,8 +110,9 @@ Refresh this section on 2026-07-24 immediately before submission. Current candid
- `v0.9.0` release candidate, with v0.8.0 already public;
- 36 adversarial tests passing locally and in remote Node.js 20, 22, and 24 CI;
- dependency-free CLI, GitHub Action, and installable Codex plugin;
-- 3 prior maintainer PRs merged with green CI; add the v0.9 PR after merge;
+- 5 maintainer PRs merged with green CI, including the v0.9 implementation PR;
- 2 stars, 0 forks, and no verified external contributor yet;
+- public r/n8n launch thread with several substantive comments that directly shaped VF010-VF013;
- first real audit: anonymized 92-node workflow, 3 blocking findings, 57 warnings, no workflow mutation;
- Codex used across product repositioning, implementation, review, Red Team, remediation, packaging, CI, release, real-workflow audit, and the community-feedback-driven v0.9 cycle.
diff --git a/docs/community-launch-v0.9.md b/docs/community-launch-v0.9.md
new file mode 100644
index 0000000..33b90c3
--- /dev/null
+++ b/docs/community-launch-v0.9.md
@@ -0,0 +1,94 @@
+# Community launch pack β v0.9.0
+
+Publish only after the public `v0.9.0` release URL works.
+
+## Feedback provenance
+
+The v0.9 scope came from the public [original r/n8n launch thread](https://www.reddit.com/r/n8n/comments/1v3is1w/i_built_an_opensource_safety_gate_for_aigenerated/), not a speculative roadmap.
+
+| Community feedback | v0.9 response |
+|---|---|
+| Separate dangerous nodes from dangerous outcomes | VF010 and VF011 classify money, customer, privileged, and destructive-data actions. |
+| Require idempotency, approval, amount/counterparty limits, and durable audit | `outcomeContracts` verifies graph evidence for those controls. |
+| Error paths can exist but notify nobody | VF012 requires an operator-visible failure path. |
+| Writes need rollback or replay stories | VF013 requires compensation, rollback, or replay evidence. |
+| Static checks cannot replace runtime guardrails | The CLI and documentation explicitly preserve that boundary. |
+
+Captured on 2026-07-22: the thread had 3 votes and several substantive comments. Treat the comments as product evidence; do not present the vote count as broad adoption.
+
+## Reddit β r/n8n
+
+### Title
+
+I built your feedback into Vibeflow v0.9: outcome-aware preflight checks for n8n workflows
+
+### Post
+
+I shared Vibeflow here earlier and two pieces of feedback changed the direction of the project:
+
+1. A dangerous outcome is not the same thing as a dangerous node. A normal HTTP node can still issue a refund, send a payment, notify a customer, change access, or delete data.
+2. A connected error branch is not enough if it silently terminates, and a write is not production-ready without a rollback, compensation, or replay story.
+
+That feedback is now implemented in Vibeflow v0.9.
+
+Vibeflow is a local, dependency-free preflight checker for exported n8n workflow JSON. The new release adds:
+
+- `VF010`: blocks money and privileged actions without an outcome contract;
+- `VF011`: warns about uncontracted customer communications and destructive writes;
+- `VF012`: detects error paths that notify nobody;
+- `VF013`: requires compensation, rollback, or replay evidence.
+
+For a contracted action, the checker verifies structural evidence in the workflow graph:
+
+- an atomic idempotency claim cannot be bypassed;
+- a durable audit write happens before the action;
+- approval, amount, and counterparty checks have real allow/deny branches;
+- the failure notification is connected to the action's error output;
+- recovery is represented in the graph.
+
+The important boundary: this is static preflight, not runtime enforcement. The payment/customer system must still enforce authorization, limits, counterparties, durable audit, and recovery at runtime.
+
+There is a reproducible unsafe refund workflow and a passing contracted version in the repository:
+
+https://github.com/domfelipe/vibeflow-n8n
+
+Release: https://github.com/domfelipe/vibeflow-n8n/releases/tag/v0.9.0
+
+I am especially looking for anonymized examples of:
+
+- a real-world action the classifier misses;
+- a false positive where the workflow is demonstrably safe;
+- a control that looks present in JSON but can still be bypassed;
+- a recovery pattern that does not fit compensate/rollback/replay.
+
+Please remove credentials and customer data before sharing workflow fragments. The best reports will become paired unsafe/safe regression fixtures.
+
+## Short reply to the original commenters
+
+Your distinction between dangerous nodes and dangerous outcomes became the core of v0.9. The release now detects ordinary HTTP/database/message nodes by impact and validates idempotency, approval, amount/counterparty limits, durable audit, operator-visible failures, and recovery evidence. I kept runtime enforcement explicitly outside the claim. Thank you β this materially improved the project.
+
+## GitHub Discussion
+
+### Title
+
+Vibeflow v0.9: help test outcome contracts against real n8n workflows
+
+### Body
+
+Vibeflow v0.9 adds outcome-aware preflight policies for money, customer, privileged, and destructive-data actions. The implementation was driven by community feedback that ordinary nodes can still produce dangerous real-world outcomes.
+
+Please test an exported workflow and report anonymized false positives, missed actions, bypassable controls, or recovery patterns. Useful reports need a minimal unsafe case, the expected safe case, and the exported JSON fields that distinguish them.
+
+- Release: https://github.com/domfelipe/vibeflow-n8n/releases/tag/v0.9.0
+- Demo: https://github.com/domfelipe/vibeflow-n8n/blob/v0.9.0/docs/demo.md
+- False-positive report: https://github.com/domfelipe/vibeflow-n8n/issues/new?template=false-positive.yml
+- Policy proposal: https://github.com/domfelipe/vibeflow-n8n/issues/new?template=rule-proposal.yml
+
+Never attach credentials, customer data, or a private production workflow.
+
+## Evidence rules
+
+- Do not ask for artificial stars, forks, or empty PRs.
+- Record only public, attributable usage or anonymized audit outcomes.
+- Convert actionable feedback into an issue and paired unsafe/safe fixture.
+- Never publish customer workflow names, IDs, node names, credentials, or infrastructure identifiers.
diff --git a/docs/launch.md b/docs/launch.md
index 35eefd2..8ab115a 100644
--- a/docs/launch.md
+++ b/docs/launch.md
@@ -26,3 +26,5 @@ A normal HTTP, database, or messaging node can refund money, contact a customer,
> The new outcome contracts detect money, customer, privileged, and destructive-data actions β including ordinary HTTP nodes β and verify structural evidence for idempotency, approval, limits, durable audit, failure notification, and recovery. It is local, dependency-free, CI-friendly, and explicit about what still needs runtime enforcement.
Link to the repository and the safe/unsafe refund demo. Ask users for anonymized workflows, classifier false positives/negatives, and missing domain actions.
+
+Ready-to-post Reddit and GitHub Discussion copy: [community-launch-v0.9.md](community-launch-v0.9.md).
From cb4d664e984b06eef6087d67524d4c536196ed49 Mon Sep 17 00:00:00 2001
From: Felipe Domingues <53182096+domfelipe@users.noreply.github.com>
Date: Thu, 23 Jul 2026 13:01:55 -0300
Subject: [PATCH 7/9] Record v0.9.0 public release evidence (#8)
Record the public tag, released CLI and plugin smoke tests, final audit gate, and refreshed Codex for OSS evidence.
---
CHANGELOG.md | 2 +-
docs/codex-for-oss-application.md | 14 +++++++-------
docs/launch.md | 10 ++++++----
docs/release-audit.md | 19 +++++++++++--------
docs/roadmap.md | 2 +-
5 files changed, 26 insertions(+), 21 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c1b485f..879396d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,6 @@
# Changelog
-## 0.9.0 - 2026-07-24
+## 0.9.0 - 2026-07-23
- Added outcome-aware policies VF010-VF013 for money, customer, privileged, and destructive-data actions.
- Added structurally verified outcome contracts for approval, durable audit, idempotency, amount and counterparty limits, failure notification, and recovery.
diff --git a/docs/codex-for-oss-application.md b/docs/codex-for-oss-application.md
index edfc161..49e7b9b 100644
--- a/docs/codex-for-oss-application.md
+++ b/docs/codex-for-oss-application.md
@@ -14,7 +14,7 @@ Official form rechecked: 2026-07-22
Submit in **English**, even through the PT-BR form. OpenAI publishes no language requirement and no evidence that language changes selection odds. English is recommended only to reduce translation friction for a global technical review.
-Submit immediately after the v0.9.0 release on 2026-07-24. Do not wait for arbitrary star, fork, or PR targets. Vibeflow's strongest evidence is its Codex-native engineering, first real 92-node audit, and a public maintenance loop that turned community feedback into tested policy features.
+Submit now that v0.9.0 is public. Do not wait for arbitrary star, fork, or PR targets. Vibeflow's strongest evidence is its Codex-native engineering, first real 92-node audit, and a public maintenance loop that turned community feedback into tested policy features.
## Copy-and-paste form
@@ -94,7 +94,7 @@ These translations are for review only. Paste the English versions above into th
Use these links only if OpenAI requests verification; the form has no dedicated evidence field.
- Public MIT repository:
-- Executable release after Friday launch:
+- Executable release:
- Reproducible demo:
- Release and Red Team audit:
- CI history:
@@ -104,14 +104,14 @@ Use these links only if OpenAI requests verification; the form has no dedicated
## Evidence snapshot
-Refresh this section on 2026-07-24 immediately before submission. Current candidate evidence:
+Captured on 2026-07-23 after release:
- public repository with MIT license;
-- `v0.9.0` release candidate, with v0.8.0 already public;
+- public `v0.9.0` release, with v0.8.0 preserved in release history;
- 36 adversarial tests passing locally and in remote Node.js 20, 22, and 24 CI;
- dependency-free CLI, GitHub Action, and installable Codex plugin;
-- 5 maintainer PRs merged with green CI, including the v0.9 implementation PR;
-- 2 stars, 0 forks, and no verified external contributor yet;
+- 7 maintainer PRs merged with green CI, including the v0.9 implementation and release-evidence PR;
+- 4 stars, 0 forks, and no verified external contributor yet;
- public r/n8n launch thread with several substantive comments that directly shaped VF010-VF013;
- first real audit: anonymized 92-node workflow, 3 blocking findings, 57 warnings, no workflow mutation;
- Codex used across product repositioning, implementation, review, Red Team, remediation, packaging, CI, release, real-workflow audit, and the community-feedback-driven v0.9 cycle.
@@ -133,7 +133,7 @@ The approved public description is: **βa real, production-scale 92-node conver
- [x] Repository is public and not archived.
- [x] GitHub username is public.
-- [x] Role is principal maintainer.
+- [x] Role is primary maintainer.
- [x] Repository URL is correct.
- [x] Both requested benefits are selected.
- [x] All narrative answers are under 500 characters.
diff --git a/docs/launch.md b/docs/launch.md
index 8ab115a..a6cc2a0 100644
--- a/docs/launch.md
+++ b/docs/launch.md
@@ -1,6 +1,6 @@
# v0.9.0 launch checklist
-Target: Friday, 2026-07-24
+Released: 2026-07-23
## Release gate
@@ -9,9 +9,11 @@ Target: Friday, 2026-07-24
- [x] Local `npm run verify` and `npm audit --omit=dev` pass.
- [x] QA, adversarial Red Team, and GuardiΓ£o reviews are documented.
- [x] Pull request CI passes on Node.js 20, 22, and 24.
-- [ ] Release commit is merged and tagged `v0.9.0`.
-- [ ] Released CLI and pinned Codex marketplace install successfully.
-- [ ] GitHub release is published on Friday.
+- [x] Release commit is merged and tagged `v0.9.0`.
+- [x] Released CLI and pinned Codex marketplace install successfully.
+- [x] GitHub release is public and marked latest.
+
+Release:
## Positioning
diff --git a/docs/release-audit.md b/docs/release-audit.md
index aa4a4c7..31672ad 100644
--- a/docs/release-audit.md
+++ b/docs/release-audit.md
@@ -1,10 +1,10 @@
-# Release audit β v0.9.0 candidate
+# Release audit β v0.9.0
-Target release: 2026-07-24
+Released: 2026-07-23
## Decision
-**APTO COM RESSALVAS** for the Friday release, pending merge, tag, and released-install checks listed below.
+**APTO** within Vibeflow's documented static-preflight boundary.
No blocking defect remains in the reviewed static-analysis boundary. Vibeflow can verify structural evidence in an exported workflow, but it cannot enforce authorization, amount limits, counterparty identity, audit durability, or recovery behavior in the executing systems.
@@ -16,6 +16,9 @@ No blocking defect remains in the reviewed static-analysis boundary. Vibeflow ca
- Text, JSON, SARIF, configuration validation, package packing, and CLI exit behavior are exercised.
- `npm run verify`, `npm audit --omit=dev`, JSON parsing, and `git diff --check` pass locally.
- The package remains dependency-free and targets Node.js 20+.
+- The public `v0.9.0` tag resolves to release commit `7552eeca7cfdd56376eab2007988b81a9726fba4`.
+- The released GitHub CLI package passes the safe refund fixture with zero findings and reports VF010, VF012, and VF013 for the unsafe refund fixture.
+- The released Codex marketplace installs `vibeflow@vibeflow` version `0.9.0` in an isolated `CODEX_HOME`.
## Red Team
@@ -56,15 +59,15 @@ The graph checks use actual `main` edges, require dominating controls, and rejec
- an exported graph cannot prove a referenced credential, approval identity, SQL policy, external API limit, notification, or compensation works at runtime;
- custom/community nodes may need explicit impact declarations or new regression-backed adapters;
- repository owners may intentionally weaken policy outside `--locked` mode;
-- remote Node 20/22/24 CI passed; released `npx`/plugin installation can only be confirmed after the candidate is tagged.
+- remote Node 20/22/24 CI and released `npx`/plugin installation passed.
## Release blockers
- [x] Pull request CI passes on Node.js 20, 22, and 24.
-- [ ] Release commit is merged without unrelated changes.
-- [ ] `v0.9.0` tag and GitHub release are published on 2026-07-24.
-- [ ] Released CLI and Codex plugin install paths are smoke-tested.
+- [x] Release commit is merged without unrelated changes.
+- [x] `v0.9.0` tag and GitHub release are public.
+- [x] Released CLI and Codex plugin install paths are smoke-tested.
## Final gate
-There are zero known critical or high security findings in the candidate. The release remains **APTO COM RESSALVAS** until the remaining remote gates above are complete; a failed gate blocks publication or requires an immediate corrective release.
+There are zero known critical or high security findings in the released static-analysis boundary. Runtime enforcement remains explicitly outside the product claim.
diff --git a/docs/roadmap.md b/docs/roadmap.md
index 91ce4ac..44791a6 100644
--- a/docs/roadmap.md
+++ b/docs/roadmap.md
@@ -4,7 +4,7 @@
Ship the executable reset: CLI, nine configurable policies, fixtures, tests, SARIF, GitHub Action, and Codex plugin.
-## 0.9.0 β target 2026-07-24
+## 0.9.0 β shipped 2026-07-23
Separate dangerous nodes from dangerous outcomes. Add VF010-VF013, explicit outcome contracts, graph evidence for policy gates, safe/unsafe refund fixtures, and clear runtime boundaries.
From 4a4f103ddc96915e33ca8b051d51828c0c2dbe11 Mon Sep 17 00:00:00 2001
From: Felipe Domingues <53182096+domfelipe@users.noreply.github.com>
Date: Fri, 24 Jul 2026 07:30:10 -0300
Subject: [PATCH 8/9] Record post-submission runtime evidence (#9)
Document the new Reddit runtime feedback, Pisama corpus provenance, licensing boundary, and maintainer-reported Codex for OSS submission status.
---
docs/codex-for-oss-application.md | 23 +++++++++++++++++++++--
docs/community-launch-v0.9.md | 20 ++++++++++++++++++++
2 files changed, 41 insertions(+), 2 deletions(-)
diff --git a/docs/codex-for-oss-application.md b/docs/codex-for-oss-application.md
index 49e7b9b..74437ca 100644
--- a/docs/codex-for-oss-application.md
+++ b/docs/codex-for-oss-application.md
@@ -10,6 +10,10 @@ Prepared: 2026-07-22
Official form rechecked: 2026-07-22
+Submission reported complete: 2026-07-24
+
+The maintainer reported that the application was submitted. The ChatGPT-account email, OpenAI Organization ID, confirmation page, and exact submission timestamp remain private and are not stored in this repository.
+
## Recommendation
Submit in **English**, even through the PT-BR form. OpenAI publishes no language requirement and no evidence that language changes selection odds. English is recommended only to reduce translation friction for a global technical review.
@@ -100,6 +104,8 @@ Use these links only if OpenAI requests verification; the form has no dedicated
- CI history:
- Launch discussion:
- Public r/n8n feedback that shaped v0.9:
+- Public runtime feedback and anonymized-corpus offer:
+- Recorded static/runtime evidence boundary:
- Engineering PRs:
## Evidence snapshot
@@ -118,6 +124,18 @@ Captured on 2026-07-23 after release:
Do not describe maintainer PRs, the maintainer's own workflow, clones, or unattributed stars as external adoption.
+## Post-submission evidence snapshot
+
+Captured on 2026-07-24:
+
+- an external Reddit commenter publicly validated the static-preflight/runtime-observability split;
+- the commenter reported fail-open Code execution, error-as-data, and AI-output truncation cases from a community-workflow corpus;
+- the commenter offered to run anonymized failing workflows through Vibeflow and share false positives;
+- the linked Pisama node and runtime project were independently verified, including their separate MIT and fair-code licensing boundaries;
+- no workflow fixture, false-positive result, PR, contributor relationship, partnership, or integration had been received or established at capture time.
+
+This is evidence of substantive community engagement and a prospective evaluation path, not evidence of adoption or contribution.
+
## Confidentiality boundary
Do not submit or link the full real-workflow audit. Application materials must not include:
@@ -139,10 +157,11 @@ The approved public description is: **βa real, production-scale 92-node conver
- [x] All narrative answers are under 500 characters.
- [x] Real-workflow evidence is anonymized.
- [x] Early adoption is described honestly.
-- [ ] Insert the exact ChatGPT-account email.
-- [ ] Insert and verify the OpenAI Organization ID.
+- [x] Supply the exact ChatGPT-account email privately in the form; do not retain it in the repository.
+- [x] Supply the OpenAI Organization ID privately in the form; do not retain it in the repository.
- [ ] Re-read the current Program Terms immediately before submission.
- [ ] Save the confirmation page and submission timestamp privately.
+- [x] Application submission reported complete by the maintainer on 2026-07-24.
## After submission
diff --git a/docs/community-launch-v0.9.md b/docs/community-launch-v0.9.md
index 33b90c3..09e2bda 100644
--- a/docs/community-launch-v0.9.md
+++ b/docs/community-launch-v0.9.md
@@ -16,6 +16,26 @@ The v0.9 scope came from the public [original r/n8n launch thread](https://www.r
Captured on 2026-07-22: the thread had 3 votes and several substantive comments. Treat the comments as product evidence; do not present the vote count as broad adoption.
+## Post-release runtime feedback β 2026-07-24
+
+Reddit user [`Fit_Preference_1795`](https://www.reddit.com/r/n8n/comments/1v3is1w/comment/ozbyh0f/) publicly described the runtime counterpart to Vibeflow's static checks and offered to test anonymized failing workflows against Vibeflow. The report identified three failure classes:
+
+- a `continueOnFail` Code node crash that leaves the execution marked successful;
+- an error object flowing through an item's JSON as ordinary data;
+- truncated AI output reaching downstream nodes despite valid JSON and a successful node status.
+
+The commenter also noted that intentional token caps must be distinguished from accidental truncation to avoid false positives. These cases reinforce the product boundary: Vibeflow can detect deterministic fail-open configuration or require a structural completeness guard, but it cannot observe runtime output or prove that an alert is watched.
+
+The linked [`n8n-nodes-pisama`](https://github.com/Pisama-AI/n8n-nodes-pisama) community node is MIT-licensed and forwards execution telemetry. Runtime detection is implemented by the separate, self-hostable [`pisama-n8n`](https://github.com/Pisama-AI/pisama-n8n) service under a fair-code license. Its public [July 2026 corpus campaign](https://github.com/Pisama-AI/pisama-n8n/blob/main/eval/campaigns/2026-07-guard-campaign.md) records 70 committed workflows, 67 real executions, and 19 observed-and-detected failures. The comment's 69-workflow report and the repository campaign are different snapshots and must not be combined into one metric.
+
+Evidence status at capture time:
+
+- verified: substantive public technical feedback and a public offer to provide anonymized false-positive evidence;
+- not yet verified: receipt of the workflows, Vibeflow verdicts, false-positive count, or a shared interoperability contract;
+- not claimed: partnership, external contribution, external user adoption, or a shipped Pisama integration.
+
+The smallest useful follow-up is three anonymized cases: fail-open Code execution, error-as-data, and intentional versus accidental truncation. A new Vibeflow rule should be added only when paired unsafe/safe fixtures expose a deterministic static signal.
+
## Reddit β r/n8n
### Title
From a3f7b784de824a4686e2fa4d7510a91883a43f9f Mon Sep 17 00:00:00 2001
From: Felipe Domingues <53182096+domfelipe@users.noreply.github.com>
Date: Thu, 6 Aug 2026 22:03:48 -0300
Subject: [PATCH 9/9] ci: add Forgejo quality workflow (mirror-friendly)
Runs on self-hosted Forgejo Actions; ignored by GitHub Actions.
GitHub remains source of truth.
---
.forgejo/workflows/quality.yml | 48 ++++++++++++++++++++++++++++++++++
1 file changed, 48 insertions(+)
create mode 100644 .forgejo/workflows/quality.yml
diff --git a/.forgejo/workflows/quality.yml b/.forgejo/workflows/quality.yml
new file mode 100644
index 0000000..ed6be61
--- /dev/null
+++ b/.forgejo/workflows/quality.yml
@@ -0,0 +1,48 @@
+# Roda no Forgejo (homelab). GitHub Actions ignora .forgejo/
+# Source of truth continua no GitHub β sΓ³ faΓ§a push no GH.
+name: quality
+
+on:
+ push:
+ pull_request:
+
+jobs:
+ quality:
+ runs-on: docker
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Detect stack
+ id: stack
+ run: |
+ set -e
+ echo "repo=$(basename "$GITHUB_REPOSITORY")"
+ if [ -f package.json ]; then echo "has_node=true" >> "$GITHUB_OUTPUT"; else echo "has_node=false" >> "$GITHUB_OUTPUT"; fi
+ if [ -f pyproject.toml ] || [ -f requirements.txt ] || [ -f setup.py ]; then echo "has_py=true" >> "$GITHUB_OUTPUT"; else echo "has_py=false" >> "$GITHUB_OUTPUT"; fi
+ if [ -f Cargo.toml ]; then echo "has_rust=true" >> "$GITHUB_OUTPUT"; else echo "has_rust=false" >> "$GITHUB_OUTPUT"; fi
+ if [ -f go.mod ]; then echo "has_go=true" >> "$GITHUB_OUTPUT"; else echo "has_go=false" >> "$GITHUB_OUTPUT"; fi
+ ls -la
+
+ - name: Node check (if any)
+ if: steps.stack.outputs.has_node == 'true'
+ run: |
+ if command -v node >/dev/null 2>&1; then node -v; else echo "node n/a in image β skip"; fi
+ if [ -f package-lock.json ] || [ -f pnpm-lock.yaml ] || [ -f yarn.lock ] || [ -f bun.lockb ] || [ -f bun.lock ]; then
+ echo "lockfile present"
+ fi
+ # ponytail: no install/test yet β add when repo has standard scripts
+ if [ -f package.json ] && command -v node >/dev/null 2>&1; then
+ node -e "const p=require('./package.json'); console.log('name=', p.name||'(none)', 'scripts=', Object.keys(p.scripts||{}).join(','))"
+ fi
+
+ - name: Python check (if any)
+ if: steps.stack.outputs.has_py == 'true'
+ run: |
+ if command -v python3 >/dev/null 2>&1; then python3 --version; else echo "python n/a"; fi
+
+ - name: Quality gate (smoke)
+ run: |
+ echo "quality OK on Forgejo runner"
+ echo "sha=${GITHUB_SHA}"
+ echo "ref=${GITHUB_REF}"
\ No newline at end of file