A demonstration of AI-driven agentic loops that keep a multi-project stack consistent with a shared data contract. When you modify the database schema, the system cascades updates through the API layer and into the UI — with developer approval at each step.
The pipeline is config-driven and goal-oriented, not hard-coded per change type. Nothing in the code says "when a field is added, do X." Instead, a declarative registry describes relationships (who consumes which contract, who owns which files, what invariant each must maintain), and a single generic loop reconciles the code to any contract change by reasoning toward that goal and verifying it.
database-project/schema.json
│ (contract)
▼
Generic Agentic Loop ── reconciles ──► api-project/routes/persons.js (LLM, goal-driven)
(node: "api") ── produces ──► api-project/swagger.json (deterministic)
│ (contract)
▼
Generic Agentic Loop ── reconciles ──► ui-project/app/page.tsx (LLM, goal-driven)
(node: "ui")
Each participant is a node in contracts.config.json. A node declares only relationships:
consumes— the upstream contract file it watchesowns— the source files the reconciler may editgoal— a one-line, natural-language invariant to maintainverify— a command that must pass after a change (the safety net)producer/produces— an optional deterministic step that regenerates a downstream contract
When a watched contract changes, the same generic loop runs for every node:
- Detect — a structural diff decides whether the contract actually changed (no field vocabulary baked in, so type changes, renames, and nested reshapes all count).
- Approve — the developer is prompted in the terminal.
- Reconcile — the LLM is handed the goal, the before/after contract, and the current file, and returns the updated file. It is not told what to do — it reasons toward the goal.
- Verify → retry → revert — the node's
verifycommand runs. If it fails, the error is fed back and the model tries again (up tomaxReconcileAttempts). If it still can't converge, every owned file is reverted. Verification, not a scripted prompt, is what keeps the change safe. - Produce — on success, any deterministic
producerregenerates the downstream contract (e.g.swagger.json), which cascades to the next node.
Deterministic contract production stays deterministic (Swagger is generated by a pure function); the LLM only reconciles owned source. Adding a new consumer to the pipeline is a matter of adding a node to the registry — no new orchestration code.
Append an entry to contracts.config.json, for example a second consumer of the Swagger contract:
Then start it with the generic loop (three lines, mirroring api-project/agentic-loop.js):
const { getNode } = require('./shared/registry');
const { startNode } = require('./shared/agentic-loop');
startNode(getNode('sdk'));├── contracts.config.json # Declarative registry: the pipeline's nodes and their relationships
├── database-project/ # NeDB database with schema definition
│ ├── schema.json # Source of truth for data shape (the root contract)
│ ├── db.js # Database access layer
│ └── seed.js # Seeds sample data
├── api-project/ # Express REST API
│ ├── server.js # API server
│ ├── routes/ # Route handlers (owned + reconciled by the "api" node)
│ ├── swagger.json # OpenAPI spec (deterministically produced from schema.json)
│ ├── swagger-generator.js # Pure schema → OpenAPI generator (the "api" node's producer)
│ └── agentic-loop.js # Thin entry point: startNode(getNode('api'))
├── ui-project/ # Next.js React UI
│ ├── components/ # PersonTable component
│ ├── app/page.tsx # Table page (owned + reconciled by the "ui" node)
│ └── agentic-loop.js # Thin entry point: startNode(getNode('ui'))
├── shared/ # The generic engine (project-agnostic)
│ ├── registry.js # Loads contracts.config.json, resolves nodes
│ ├── agentic-loop.js # Generic watch → detect → approve → reconcile → produce loop
│ ├── reconcile.js # Generic goal-driven reconcile + verify + retry + revert
│ ├── contract-diff.js # Generic structural contract diff / change detection
│ ├── approval-prompt.js# Terminal y/n approval
│ ├── llm-client.js # OpenAI-compatible LLM client
│ ├── file-watcher.js # File change detection (chokidar)
│ └── notifier.js # macOS notifications
├── scripts/ # Helper scripts
│ ├── start-demo.js # Starts all services
│ ├── add-field.js # Adds a field to schema + generates data
│ └── remove-field.js # Removes a field from schema + data
└── tests/ # Integration tests
# Clone and install dependencies
git clone <repo-url>
cd selfHealingAI
npm install
# Configure environment
cp .env.example .env
# Edit .env with your LLM server address and model name
# Seed the database with sample data
npm run --workspace=database-project seed# Start the agentic loops (watches for changes)
npm run demo
# In a separate terminal, start the API server
npm run start:api
# In a separate terminal, start the Next.js UI
cd ui-project && npm run devAdd a field to the schema:
# Adds a field and generates sample data via LLM
node scripts/add-field.js date_of_birth string
node scripts/add-field.js age numberRemove a field:
node scripts/remove-field.js date_of_birthAfter a schema change, each node's loop will:
- Detect that the contract actually changed
- Prompt for approval in the terminal (y/n)
- Reconcile its owned files toward the node's goal via the LLM
- Run the node's
verifycommand; retry with the failure fed back, or revert if it can't converge - On success, regenerate any downstream contract — cascading to the next node
| Variable | Default | Description |
|---|---|---|
LLM_ENDPOINT |
http://localhost:8080/v1/chat/completions |
OpenAI-compatible completions URL |
LLM_MODEL |
default |
Model name to pass to the LLM server |
PORT |
3000 |
API server port |
# Run all tests (unit + property-based)
npm test
# Run UI tests only
npm test --workspace=ui-projectISC
{ "id": "sdk", "project": "SDK_Project", "consumes": "api-project/swagger.json", "contractPath": ["components", "schemas", "Person", "properties"], "owns": ["sdk-project/src/person.ts"], "goal": "The Person interface in person.ts must have one property per field in the OpenAPI Person schema, with matching types.", "verify": "npm run --workspace=sdk-project typecheck", "maxReconcileAttempts": 2 }