Chapter 13: Agent AI, Loop Engineering & Harnessing
The highest-paid engineers in 2026 don't write more code. They write systems that write code.
Let that land.
While you've been grinding through sprint tickets, debugging race conditions in your Express middleware, and arguing about whether to use Promise.all or for...of in code review, a parallel universe of engineering has been forming. In that universe, engineers don't write individual functions. They write agents — autonomous programs that reason, use tools, observe results, and iterate. They don't debug manually. They build loops that debug themselves. They don't review PRs line by line. They build harnesses that review a thousand PRs while they sleep.
This chapter is about that universe. And by the end of it, you'll know how to enter it.
The gap is real. You've felt it. You see job descriptions asking for "AI-native engineering" and "agent orchestration" and you wonder: Is that just prompt engineering with extra steps? It's not. The engineers commanding ₹80L to ₹1.2Cr at Razorpay, Zepto, and Freshworks right now aren't better prompt writers than you. They're systems thinkers who treat LLMs as a new kind of runtime — unpredictable, non-deterministic, and wildly powerful when harnessed correctly.
This chapter will teach you to build that harness.
The Runtime Has Changed
Before we write a single agent, we need to understand what changed. For twenty years, the contract between you and your code was simple: deterministic inputs produce deterministic outputs. add(2, 3) returns 5. Always. On every machine. Forever.
LLMs broke that contract.
When you call openai.chat.completions.create(), the same input can produce different outputs. The model might hallucinate. It might refuse. It might produce brilliant code on Tuesday and lazy shortcuts on Wednesday. This isn't a bug. It's a property of the runtime. And just like you learned to handle network failures, race conditions, and eventual consistency, you now need to learn to handle non-deterministic intelligence.
The engineers who internalize this shift first are the ones who get paid.
Here's the mental model: LLMs are not databases. They are not APIs. They are junior engineers with infinite energy, zero judgment, and a tendency to confidently produce wrong answers. Your job is no longer to write the code. Your job is to build the system that manages these junior engineers — assigning work, reviewing output, correcting mistakes, and escalating when they're stuck.
That system is called an agent.
Agent Architecture: The ReAct Pattern
The foundational pattern in agent engineering is called ReAct — Reasoning + Acting. It was introduced in a 2022 paper, but by 2026 it's the substrate beneath every production agent system. If you understand nothing else from this chapter, understand ReAct.
Here's the problem ReAct solves: an LLM, on its own, can only generate text. It can't check the time, query a database, read a file, or deploy code. It's a brain in a jar. ReAct gives that brain hands.
The pattern works like this:
- Observe: The agent receives an input — a user query, a system alert, a scheduled trigger.
- Think: The LLM reasons about what it knows and what it needs to know. It produces a thought and decides on an action.
- Act: The agent executes a tool — runs a shell command, queries an API, reads a file, writes code.
- Observe: The tool's output becomes new context. The cycle repeats.
- Finish: When the agent has enough information, it produces a final answer.
Let's build one. Not a toy. A real agent that reviews code.
// agent-core.js — The ReAct loop, stripped to its essence
const MAX_ITERATIONS = 10;
async function reactLoop(llm, tools, userQuery) {
const messages = [
{ role: 'system', content: buildSystemPrompt(tools) },
{ role: 'user', content: userQuery }
];
for (let i = 0; i < MAX_ITERATIONS; i++) {
const response = await llm.chat({ messages });
const action = parseAction(response.content);
if (!action) {
// No tool call — agent is done thinking
return response.content;
}
// Execute the tool
const toolResult = await executeTool(tools, action);
messages.push(
{ role: 'assistant', content: response.content },
{ role: 'user', content: `Tool result: ${toolResult}` }
);
}
throw new Error('Agent exceeded max iterations — likely stuck in a loop');
}
This is the entire architecture. Thirty lines. The magic isn't in the loop — it's in the tools you give it and the system prompt that constrains its behavior.
Here's the system prompt that makes it work:
function buildSystemPrompt(tools) {
const toolDescriptions = tools.map(t =>
`- ${t.name}: ${t.description}\n Parameters: ${JSON.stringify(t.parameters)}`
).join('\n');
return `You are an autonomous agent. You have access to the following tools:
${toolDescriptions}
To use a tool, respond with EXACTLY this format:
<tool_call>
<name>tool_name</name>
<params>{ "param1": "value1" }</params>
</tool_call>
If you have enough information to answer the user's question, respond directly
without a tool call. Think step by step. If a tool fails, try a different approach.`;
}
The system prompt is the most underrated piece of agent engineering. A vague prompt produces a wandering agent. A precise prompt — with explicit output formats, failure handling instructions, and stopping conditions — produces a reliable one. We'll return to this.
But first, let's give our agent real tools.
Building a Code Review Agent
Here's a concrete agent that reviews pull requests. This is the kind of system that, once built, saves your team 15-20 hours of review time per week. The kind of system that makes a VP of Engineering ask, "Who built this?"
// tools/code-review-tools.js
const { execSync } = require('child_process');
const fs = require('fs/promises');
const codeReviewTools = [
{
name: 'get_diff',
description: 'Get the git diff for a PR. Returns changed files and their diffs.',
parameters: {
baseBranch: { type: 'string', required: true },
prBranch: { type: 'string', required: true }
},
async execute({ baseBranch, prBranch }) {
const diff = execSync(
`git diff ${baseBranch}...${prBranch} -- . ':!package-lock.json' ':!node_modules'`,
{ maxBuffer: 10 * 1024 * 1024, encoding: 'utf-8' }
);
return diff.slice(0, 50000); // Truncate for context window
}
},
{
name: 'read_file',
description: 'Read the full contents of a specific file at a given ref.',
parameters: {
filePath: { type: 'string', required: true },
ref: { type: 'string', required: true }
},
async execute({ filePath, ref }) {
const content = execSync(`git show ${ref}:${filePath}`, {
encoding: 'utf-8', maxBuffer: 5 * 1024 * 1024
});
return content;
}
},
{
name: 'run_tests',
description: 'Run the test suite and return results.',
parameters: {
testPattern: { type: 'string', required: false }
},
async execute({ testPattern }) {
try {
const pattern = testPattern || '';
const result = execSync(`npx jest ${pattern} --json --forceExit`, {
encoding: 'utf-8', maxBuffer: 5 * 1024 * 1024
});
return JSON.stringify({ passed: true, output: result });
} catch (err) {
return JSON.stringify({
passed: false,
output: err.stdout?.toString() || err.message
});
}
}
},
{
name: 'check_types',
description: 'Run TypeScript type checking and return errors.',
parameters: {},
async execute() {
try {
execSync('npx tsc --noEmit', { encoding: 'utf-8' });
return 'Type checking passed — no errors.';
} catch (err) {
return err.stdout?.toString() || err.message;
}
}
}
];
Now the review prompt — this is where most agents fail. A lazy prompt like "review this code" produces generic feedback. A precise prompt produces actionable, specific, ranked findings:
const REVIEW_SYSTEM_PROMPT = `You are a senior code reviewer for a Node.js team at a high-growth Indian startup.
Your reviews are direct, specific, and actionable. You do not compliment code that works —
you find what will break in production.
## Review Protocol
1. First, call get_diff to see what changed.
2. For any file with significant changes, call read_file to see full context.
3. If the PR changes business logic, call run_tests.
4. If the project uses TypeScript, call check_types.
## Output Format
After gathering all evidence, produce a review with these sections:
### CRITICAL (must fix before merge)
- Bugs that will cause incorrect behavior or crashes
- Security vulnerabilities (injection, auth bypass, exposed secrets)
- Data loss risks
### HIGH (should fix before merge)
- Race conditions, memory leaks, unhandled rejections
- Missing error handling on external calls
- N+1 queries or O(n²) operations on large datasets
### MEDIUM (fix in follow-up PR)
- Missing tests for new behavior
- Overly complex functions (suggest simplifications)
- Inconsistent patterns with the rest of the codebase
### LOW (nice to have)
- Naming suggestions
- Minor performance improvements
## Rules
- Every finding MUST reference a specific file and line number.
- If you're unsure whether something is a bug, flag it as HIGH with your uncertainty noted.
- Do NOT comment on formatting — the linter handles that.
- If the PR is trivial (docs, config, deps), say so and keep the review short.`;
Now wire it together:
// code-review-agent.js
async function reviewPR(baseBranch, prBranch) {
const agent = createAgent({
model: 'claude-sonnet-4-20250514',
tools: codeReviewTools,
systemPrompt: REVIEW_SYSTEM_PROMPT,
maxIterations: 8
});
const result = await agent.run(
`Review the PR merging ${prBranch} into ${baseBranch}. ` +
`Be thorough. Find every bug.`
);
return result;
}
This agent doesn't just read a diff and spit out opinions. It reads files for context. It runs tests. It checks types. It builds a mental model of the change before producing findings. That's the difference between a toy and a tool.
Rahul, a staff engineer at a Bangalore fintech startup, built a version of this agent in Q1 2026. His team of 12 engineers was spending 8-10 hours per week on code review. The agent now handles the first pass — catching 60% of bugs before a human ever looks at the PR. The team's review time dropped to 3-4 hours per week. Rahul got promoted to Principal Engineer in the next cycle. His agent didn't replace human reviewers. It made them dramatically more effective by eliminating the grunt work.
Tool Design: The Difference Between a Useful Agent and a Useless One
Here's a truth most agent tutorials skip: the quality of your agent is 80% determined by the quality of your tools. A brilliant LLM with bad tools is a frustrated genius. A mediocre LLM with great tools is a productive team member.
Good tools follow these rules:
1. Narrow scope. A tool should do one thing. get_diff is better than analyze_repository. The agent can compose narrow tools; it can't decompose a monolithic one.
2. Structured output. Return JSON, not prose. The agent needs to parse your tool's output to decide its next action. A wall of text is noise.
3. Error messages that guide recovery. Don't return "Error: permission denied." Return "Error: permission denied on /var/log/app.log. Try running with sudo, or check file ownership with ls -l." The agent will use that hint to self-correct.
4. Idempotent where possible. A tool called twice with the same inputs should produce the same result. Non-deterministic tools (like run_tests against a live DB) need clear documentation so the agent knows to be careful.
5. Bounded cost. Every tool should have a cost ceiling — timeouts, result size limits, rate limits. An agent stuck in a loop calling an expensive tool is a very fast way to burn through your API budget.
Here's a tool that violates every rule:
// BAD: Monolithic, unstructured, no error guidance
const badTool = {
name: 'fix_everything',
description: 'Fix all problems in the codebase',
async execute() {
// Runs 47 different checks, takes 12 minutes,
// returns 80MB of unstructured text
}
};
And here's the same capability, decomposed:
// GOOD: Narrow, structured, self-documenting
const goodTools = [
{
name: 'lint_file',
description: 'Run ESLint on a single file. Returns JSON array of violations.',
parameters: { filePath: { type: 'string', required: true } },
async execute({ filePath }) {
const result = execSync(`npx eslint ${filePath} --format json`, {
encoding: 'utf-8'
});
const parsed = JSON.parse(result);
return JSON.stringify(parsed[0]?.messages || []);
}
},
{
name: 'check_test_coverage',
description: 'Get test coverage for a specific file. Returns { covered: number, total: number, uncoveredLines: number[] }.',
parameters: { filePath: { type: 'string', required: true } },
async execute({ filePath }) {
// Implementation
}
},
{
name: 'detect_security_issues',
description: 'Scan a file for common Node.js security issues. Returns JSON array of { severity, line, description, fix }.',
parameters: { filePath: { type: 'string', required: true } },
async execute({ filePath }) {
// Implementation
}
}
];
The agent can now decide: "I'll lint first. If there are violations, I'll fix them. Then I'll check coverage. Then I'll scan for security issues." Each step is small, fast, and produces structured data the agent can reason about.
Loop Engineering: When Agents Self-Correct
A single pass through the ReAct loop is useful. But the real power — the thing that separates ₹60L engineers from ₹1Cr engineers — is building agents that iterate on their own output.
This is loop engineering. The agent doesn't just produce an answer. It produces an answer, evaluates it, finds flaws, and improves it. Over and over. Until the output meets a quality bar.
Think about how you write production code. You don't write it once and ship it. You write a draft. You run it. It fails. You debug. You fix. You run it again. You refactor. You add tests. You ship. That's a loop. Loop engineering automates that loop.
Here's the pattern:
// loop-engine.js — Self-correcting agent loop
async function selfCorrectingLoop(llm, tools, evaluator, userQuery, qualityThreshold) {
let currentOutput = null;
let currentScore = 0;
let iteration = 0;
const MAX_SELF_CORRECT_ITERATIONS = 5;
// Phase 1: Generate initial output
const initialResult = await reactLoop(llm, tools, userQuery);
currentOutput = initialResult;
while (iteration < MAX_SELF_CORRECT_ITERATIONS) {
// Phase 2: Evaluate
const evaluation = await evaluator.evaluate(currentOutput, userQuery);
currentScore = evaluation.score;
if (currentScore >= qualityThreshold) {
break; // Good enough — ship it
}
// Phase 3: Generate critique and improvement plan
const critique = evaluation.critique;
const improvementPrompt = `
Your previous output scored ${currentScore}/100. Required: ${qualityThreshold}/100.
Critique: ${critique}
Please improve your output. Address every point in the critique.
Do NOT change things that were already correct.`;
// Phase 4: Regenerate with critique as context
const improvedResult = await reactLoop(
llm, tools,
`${userQuery}\n\nPrevious attempt:\n${currentOutput}\n\n${improvementPrompt}`
);
currentOutput = improvedResult;
iteration++;
}
return { output: currentOutput, score: currentScore, iterations: iteration };
}
The evaluator is the key component. It can be another LLM call (an "LLM-as-judge" pattern), a set of deterministic checks, or a combination:
// evaluators/code-quality-evaluator.js
async function evaluateCodeQuality(code, requirements) {
const checks = [];
// Deterministic checks
checks.push({ name: 'parse_check', passed: validateJSParse(code), weight: 10 });
checks.push({ name: 'type_check', passed: await runTypeCheck(code), weight: 15 });
checks.push({ name: 'test_pass', passed: await runTests(code), weight: 25 });
checks.push({ name: 'lint_check', passed: await runLint(code), weight: 10 });
// LLM-based checks
const llmEval = await evaluateWithLLM(code, requirements);
checks.push({ name: 'correctness', passed: llmEval.correctness, weight: 20 });
checks.push({ name: 'performance', passed: llmEval.performance, weight: 10 });
checks.push({ name: 'security', passed: llmEval.security, weight: 10 });
const score = checks.reduce((sum, c) => sum + (c.passed ? c.weight : 0), 0);
const critique = checks
.filter(c => !c.passed)
.map(c => `${c.name}: FAILED`)
.join('\n');
return { score, critique, checks };
}
This is not theoretical. This is how production AI systems work in 2026. The self-correcting loop catches hallucinations, fixes syntax errors, and improves code quality — all without a human in the loop. The human reviews the final output, not every intermediate draft.
The Observe-Plan-Act Cycle
ReAct is the engine. Self-correction is the transmission. But the chassis — the structure that holds everything together — is the Observe-Plan-Act (OPA) cycle.
OPA is a higher-level loop that wraps the ReAct pattern. It's what you use when the task is too large for a single agent session — when you need to break work into phases, track progress across phases, and make strategic decisions about what to do next.
OBSERVE → What is the current state? What changed since last cycle?
PLAN → Given the state, what should I do next? What tools do I need?
ACT → Execute the plan. Delegate to sub-agents if needed.
↓
(repeat until goal state reached)
Here's a concrete implementation for an incident response agent:
// incident-response-agent.js
const INCIDENT_RESPONSE_PLAN = {
goals: [
'Identify the root cause of the incident',
'Mitigate customer impact',
'Implement a fix or rollback',
'Verify the fix resolved the issue',
'Document the incident for postmortem'
],
maxCycles: 10,
tools: [
'query_datadog', 'query_pagerduty', 'check_deployment_status',
'read_service_logs', 'run_health_check', 'rollback_deployment',
'scale_service', 'clear_cache', 'restart_service',
'notify_slack', 'create_incident_doc'
]
};
async function incidentResponseLoop(alert) {
const state = {
alert,
observations: [],
actionsTaken: [],
currentHypothesis: null,
goalIndex: 0
};
for (let cycle = 0; cycle < INCIDENT_RESPONSE_PLAN.maxCycles; cycle++) {
// OBSERVE
const observations = await gatherObservations(state);
state.observations.push(...observations);
// PLAN
const plan = await generatePlan(state, INCIDENT_RESPONSE_PLAN);
if (plan.action === 'DONE') break;
if (plan.action === 'ESCALATE') {
await notifySlack('@oncall-engineer', plan.escalationReason);
break;
}
// ACT
const result = await executeAction(plan, state);
state.actionsTaken.push({ action: plan.action, result, timestamp: Date.now() });
// Check if current goal is achieved
const goalAchieved = await checkGoal(state, INCIDENT_RESPONSE_PLAN.goals[state.goalIndex]);
if (goalAchieved) {
state.goalIndex++;
if (state.goalIndex >= INCIDENT_RESPONSE_PLAN.goals.length) break;
}
}
return generateIncidentReport(state);
}
The OPA cycle is what separates a script from a system. A script runs once and hopes for the best. A system observes, plans, acts, and adapts.
Multi-Agent Systems: When One Agent Isn't Enough
Here's a pattern that will define the next five years of backend engineering: multi-agent systems. Instead of one monolithic agent trying to do everything, you deploy specialized agents that collaborate.
Think of it like a microservices architecture for intelligence.
A single agent handling incident response, code review, deployment, and documentation is like a single Express server handling auth, payments, notifications, and analytics. It works until it doesn't. Then it fails catastrophically.
The multi-agent pattern decomposes responsibility:
Orchestrator Agent
├── Code Review Agent (specialized in bug detection)
├── Security Audit Agent (specialized in vulnerability scanning)
├── Performance Agent (specialized in bottleneck detection)
├── Test Generation Agent (specialized in writing tests)
└── Documentation Agent (specialized in generating docs)
Each agent has its own system prompt, its own tools, its own evaluation criteria. The orchestrator routes work, aggregates results, and resolves conflicts.
// multi-agent-orchestrator.js
const agents = {
'code-review': createCodeReviewAgent(),
'security-audit': createSecurityAuditAgent(),
'performance': createPerformanceAgent(),
'test-gen': createTestGenerationAgent(),
};
async function orchestratePRReview(prDetails) {
// Phase 1: Parallel execution of independent agents
const [reviewResult, securityResult, perfResult] = await Promise.all([
agents['code-review'].run(prDetails),
agents['security-audit'].run(prDetails),
agents['performance'].run(prDetails),
]);
// Phase 2: Synthesize findings
const synthesisPrompt = `
Synthesize the following findings into a single, deduplicated review:
## Code Review Findings
${reviewResult}
## Security Findings
${securityResult}
## Performance Findings
${perfResult}
Rules:
- Remove duplicate findings (same file, same line, same issue).
- If two agents disagree, flag the conflict explicitly.
- Rank all findings by severity: CRITICAL > HIGH > MEDIUM > LOW.
- Produce a single, unified review.`;
const finalReview = await llm.chat({ messages: [{ role: 'user', content: synthesisPrompt }] });
// Phase 3: Generate tests for changed code
const testResult = await agents['test-gen'].run({
changedFiles: prDetails.changedFiles,
reviewFindings: finalReview
});
return {
review: finalReview,
generatedTests: testResult,
metadata: {
agentsInvoked: Object.keys(agents),
timestamp: new Date().toISOString()
}
};
}
The orchestrator pattern gives you three things that a single agent cannot: parallelism (multiple agents work simultaneously), specialization (each agent is optimized for one task), and fault isolation (a hallucinating security agent doesn't corrupt the code review).
Priya, a platform engineer at a Mumbai-based SaaS company, built a multi-agent PR review system in early 2026. Her orchestrator runs four agents on every PR. The system catches bugs that individual senior engineers miss — not because the agents are smarter, but because they're systematic. They never get tired. They never skip a check because it's 11 PM and they want to go home. Priya's system now reviews 40+ PRs per day. She was promoted to Architect in March.
Harnessing: Building the Meta-Systems
Agents are the workers. Loops are the process. But the factory — the system that manages agents at scale — is what I call harnessing.
Harnessing is the meta-layer. It's CI/CD for AI. It's automated testing of LLM outputs. It's prompt regression testing. It's the infrastructure that makes agents reliable enough to trust in production.
Let's build each piece.
Prompt Regression Testing
Your agent's behavior is governed by a system prompt. Change one sentence in that prompt, and the agent might start producing subtly different — or catastrophically wrong — output. You need to test prompts the way you test code.
// prompt-regression-test.js
const PROMPT_TEST_SUITE = [
{
name: 'handles_empty_diff',
input: 'Review this PR: no files changed.',
expectedBehaviors: [
'mentions that no files were changed',
'does NOT fabricate issues for nonexistent code',
'responds in under 500 tokens'
]
},
{
name: 'catches_sql_injection',
input: `Review this PR diff:
+ const query = "SELECT * FROM users WHERE id = " + req.params.id;`,
expectedBehaviors: [
'flags SQL injection as CRITICAL',
'suggests parameterized queries',
'references a specific line'
]
},
{
name: 'catches_missing_await',
input: `Review this PR diff:
+ const user = User.findById(id);
+ return user.email;`,
expectedBehaviors: [
'flags missing await as HIGH or CRITICAL',
'explains that user will be a Promise, not a document'
]
},
{
name: 'handles_trivial_pr',
input: 'Review this PR: updated README.md with new badge.',
expectedBehaviors: [
'identifies the PR as trivial',
'keeps review under 200 tokens',
'does NOT suggest code changes'
]
}
];
async function runPromptRegression(agent, testSuite) {
const results = [];
for (const test of testSuite) {
const output = await agent.run(test.input);
const evaluations = await Promise.all(
test.expectedBehaviors.map(async (behavior) => {
const check = await llm.chat({
messages: [{
role: 'user',
content: `Does this text satisfy: "${behavior}"?\n\nText:\n${output}\n\nAnswer ONLY "YES" or "NO".`
}]
});
return { behavior, passed: check.content.trim().toUpperCase() === 'YES' };
})
);
results.push({
test: test.name,
passed: evaluations.every(e => e.passed),
evaluations,
output: output.slice(0, 500) // Truncate for report
});
}
return results;
}
Run this in CI. Every time someone proposes a prompt change, the regression suite runs. If the agent's behavior changes unexpectedly, the PR is blocked. This is not optional. This is the minimum bar for production AI systems.
CI/CD for AI: The Agent Pipeline
Your agents need the same rigor you apply to your application code. Version control. Staging environments. Canary deployments. Rollback capability.
# .github/workflows/agent-pipeline.yml
name: Agent CI/CD Pipeline
on:
pull_request:
paths:
- 'agents/**'
- 'prompts/**'
- 'tools/**'
jobs:
prompt-regression:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run prompt regression tests
run: node scripts/run-prompt-tests.js
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
agent-evaluation:
runs-on: ubuntu-latest
needs: prompt-regression
steps:
- uses: actions/checkout@v4
- name: Run agent eval harness
run: node scripts/run-agent-eval.js --dataset=production-mirror
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
deploy-staging:
runs-on: ubuntu-latest
needs: agent-evaluation
steps:
- name: Deploy agent to staging
run: node scripts/deploy-agent.js --env=staging
smoke-test:
runs-on: ubuntu-latest
needs: deploy-staging
steps:
- name: Run smoke tests against staging agent
run: node scripts/smoke-test-agent.js --env=staging
deploy-production:
runs-on: ubuntu-latest
needs: smoke-test
steps:
- name: Deploy agent to production
run: node scripts/deploy-agent.js --env=production --canary=10%
This pipeline catches prompt regressions, evaluates agent quality against a golden dataset, deploys to staging, smoke-tests, and then canary-deploys to production. The same rigor you apply to your Node.js services now applies to your AI systems.
Automated Testing of LLM Outputs
LLM outputs are non-deterministic. You cannot test them with assert.equal(). You need a new category of assertions: behavioral assertions.
// llm-assertions.js
const llmAssert = {
async containsConcept(output, concept) {
const check = await llm.chat({
messages: [{
role: 'user',
content: `Does the following text discuss or reference the concept of "${concept}"?\n\nText:\n${output}\n\nAnswer ONLY "YES" or "NO".`
}]
});
if (check.content.trim().toUpperCase() !== 'YES') {
throw new Error(`Expected output to contain concept: "${concept}"`);
}
},
async doesNotContain(output, forbidden) {
const check = await llm.chat({
messages: [{
role: 'user',
content: `Does the following text contain "${forbidden}"?\n\nText:\n${output}\n\nAnswer ONLY "YES" or "NO".`
}]
});
if (check.content.trim().toUpperCase() === 'YES') {
throw new Error(`Output contains forbidden content: "${forbidden}"`);
}
},
async isActionable(output) {
const check = await llm.chat({
messages: [{
role: 'user',
content: `Does the following text contain specific, actionable steps (not just general advice)?\n\nText:\n${output}\n\nAnswer ONLY "YES" or "NO".`
}]
});
if (check.content.trim().toUpperCase() !== 'YES') {
throw new Error('Expected output to be actionable');
}
},
async matchesTone(output, expectedTone) {
const check = await llm.chat({
messages: [{
role: 'user',
content: `Is the tone of the following text "${expectedTone}"?\n\nText:\n${output}\n\nAnswer ONLY "YES" or "NO".`
}]
});
if (check.content.trim().toUpperCase() !== 'YES') {
throw new Error(`Expected tone "${expectedTone}" but got different tone`);
}
}
};
These assertions are slow and cost money. Run them in CI, not on every save. Cache results where possible. But run them. The alternative — shipping untested AI behavior to production — is how you end up on the front page of Hacker News for the wrong reasons.
Building ON the Platforms: Claude Code, Cursor, Copilot
By now you've used Claude Code, Cursor, or Copilot as a consumer. You type a comment, it suggests code. You ask a question, it answers. That's level one. That's what every engineer with an internet connection can do.
Level two is building on these platforms. Extending them. Making them do things their designers didn't anticipate.
Claude Code exposes hooks — shell scripts that run at specific points in the agent's lifecycle. You can use these to inject custom behavior:
# ~/.claude/settings.json — Claude Code hooks
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"command": "node ~/claude-hooks/validate-bash.js"
}
],
"PostToolUse": [
{
"matcher": "Write|Edit",
"command": "node ~/claude-hooks/auto-format.js"
}
],
"Notification": [
{
"matcher": "",
"command": "node ~/claude-hooks/log-activity.js"
}
],
"Stop": [
{
"matcher": "",
"command": "node ~/claude-hooks/auto-commit.sh"
}
]
}
}
The PreToolUse hook runs before Claude Code executes any bash command. You can use it to validate commands, block dangerous operations, or inject environment-specific configuration. The Stop hook runs when Claude Code finishes a task — perfect for auto-committing, running tests, or updating your task tracker.
Here's a practical hook that prevents Claude Code from accidentally pushing to main:
// ~/claude-hooks/validate-bash.js
const DANGEROUS_PATTERNS = [
{ pattern: /git push.*main/, message: 'Pushing to main is blocked. Use a feature branch.' },
{ pattern: /git push.*master/, message: 'Pushing to master is blocked. Use a feature branch.' },
{ pattern: /rm -rf/, message: 'Recursive delete is blocked. Review manually.' },
{ pattern: /DROP (TABLE|DATABASE)/i, message: 'DROP operations are blocked. Review manually.' },
{ pattern: /kubectl delete.*production/i, message: 'Production deletions are blocked.' },
];
const command = process.env.CLAUDE_TOOL_INPUT || '';
for (const { pattern, message } of DANGEROUS_PATTERNS) {
if (pattern.test(command)) {
console.log(JSON.stringify({
decision: 'block',
reason: message
}));
process.exit(0);
}
}
console.log(JSON.stringify({ decision: 'allow' }));
This is the meta-skill in action. You're not just using AI tools. You're building guardrails, extensions, and custom behaviors on top of them. You're treating the AI platform as... a platform.
Cursor exposes its capabilities through .cursorrules — a file that defines project-specific AI behavior. Most engineers write a few lines about code style. The ₹1Cr engineer writes a comprehensive specification:
// .cursorrules — Project-level AI behavior specification
You are working on a Node.js monorepo at a fintech company.
Tech stack: Node 22, TypeScript 5.5, Express 5, PostgreSQL, Redis, Kafka.
## Code Generation Rules
- Always use TypeScript. Never generate plain JavaScript.
- Prefer `async/await` over raw promises. Never use `.then()` chains.
- All database queries must use parameterized statements. Never string-interpolate SQL.
- All external API calls must have timeouts (default: 10s) and retry logic (default: 3 attempts with exponential backoff).
- Error messages must never leak internal state to the client.
- Every new service function must have a corresponding test file.
## Review Rules
- Flag any `any` type usage as HIGH severity.
- Flag any `console.log` in production code as MEDIUM severity.
- Flag any hardcoded secrets or credentials as CRITICAL.
- Flag any synchronous filesystem operations in request handlers as HIGH.
## Architecture Rules
- New services go in `packages/`. Shared utilities go in `packages/shared/`.
- Database migrations are in `packages/db/migrations/`. Never modify existing migrations.
- API routes follow the pattern: `packages/api/src/routes/{resource}.ts`.
- Event handlers follow the pattern: `packages/workers/src/handlers/{event-type}.ts`.
This file is not documentation. It's executable specification. Every time Cursor generates code, it reads this file. Every time Claude Code reviews a PR, it reads this file. You write it once, and every AI tool in your pipeline respects it.
Autonomous Systems: Self-Healing Infrastructure
Let's go deeper. Agents that review code are useful. Agents that keep your production systems alive are transformative.
Self-healing infrastructure works like this: a monitoring system detects an anomaly. Instead of paging a human, it triggers an agent. The agent investigates, diagnoses, and — if the fix is low-risk — applies it automatically. If the fix is high-risk, it drafts a remediation plan and escalates to a human with all the evidence pre-collected.
Here's a self-healing deployment pipeline:
// self-healing-deploy.js
const DEPLOYMENT_PIPELINE = {
stages: [
{ name: 'build', action: buildService, rollback: null },
{ name: 'canary-deploy', action: deployCanary, rollback: removeCanary },
{ name: 'health-check', action: runHealthChecks, rollback: null },
{ name: 'smoke-test', action: runSmokeTests, rollback: null },
{ name: 'full-deploy', action: deployFull, rollback: rollbackFull },
{ name: 'monitor', action: monitorMetrics, rollback: rollbackFull }
],
healthThresholds: {
errorRate: 0.01, // 1% max
p99Latency: 500, // 500ms max
cpuUsage: 0.80 // 80% max
}
};
async function selfHealingDeploy(version) {
const state = { version, currentStage: 0, metrics: {} };
for (let i = 0; i < DEPLOYMENT_PIPELINE.stages.length; i++) {
const stage = DEPLOYMENT_PIPELINE.stages[i];
state.currentStage = i;
try {
console.log(`[${stage.name}] Starting...`);
await stage.action(version);
console.log(`[${stage.name}] Complete.`);
} catch (err) {
console.log(`[${stage.name}] Failed: ${err.message}`);
// Trigger healing agent
const diagnosis = await healingAgent.diagnose(stage.name, err, state);
if (diagnosis.fixable && diagnosis.risk === 'low') {
console.log(`[Healing] Auto-applying fix: ${diagnosis.fix}`);
await diagnosis.apply();
// Retry the stage
await stage.action(version);
} else if (diagnosis.fixable && diagnosis.risk === 'medium') {
console.log(`[Healing] Drafting fix for human approval: ${diagnosis.fix}`);
await notifyOncall(diagnosis);
// Wait for approval or rollback
const decision = await waitForHumanDecision(diagnosis, 300000); // 5 min timeout
if (decision === 'approve') {
await diagnosis.apply();
await stage.action(version);
} else {
await rollbackToStage(state, i);
return { status: 'rolled_back', reason: 'human_decision' };
}
} else {
// High risk or unfixable — rollback
console.log(`[Healing] Rolling back: ${diagnosis.reason}`);
await rollbackToStage(state, i);
return { status: 'rolled_back', reason: diagnosis.reason };
}
}
// Post-stage health check
const health = await checkSystemHealth(DEPLOYMENT_PIPELINE.healthThresholds);
if (!health.healthy) {
console.log(`[Health] Degraded after ${stage.name}: ${health.reason}`);
const diagnosis = await healingAgent.diagnoseHealth(health, state);
// ... same healing logic
}
}
return { status: 'deployed', version };
}
The healing agent itself:
// healing-agent.js
const healingAgent = {
async diagnose(stageName, error, state) {
const prompt = `
You are a site reliability engineer diagnosing a deployment failure.
Stage: ${stageName}
Error: ${error.message}
Stack: ${error.stack}
Version: ${state.version}
Previous stages: ${JSON.stringify(state.metrics)}
Available tools: check_pod_status, read_recent_logs, check_db_connections,
check_redis, check_kafka_lag, check_cpu_memory, check_recent_deploys
Diagnose the root cause. Determine:
1. Is this fixable automatically? (yes/no)
2. Risk level of auto-fix (low/medium/high)
3. Specific fix to apply
4. Reasoning
Respond in JSON:
{
"fixable": true/false,
"risk": "low"|"medium"|"high",
"rootCause": "...",
"fix": "specific command or action",
"reasoning": "..."
}`;
const result = await reactLoop(llm, sreTools, prompt);
return JSON.parse(result);
}
};
This is not science fiction. This is running in production at Indian startups right now. Vikram, an SRE at a Delhi-based edtech company, built a self-healing pipeline in Q3 2025. Before his system, every deployment failure meant a 3 AM page and 45 minutes of groggy debugging. After his system, 70% of deployment failures are resolved automatically. The other 30% arrive at the on-call engineer's phone with a full diagnosis, a proposed fix, and all relevant logs pre-attached. Mean time to resolution dropped from 45 minutes to 8 minutes. Vikram's next job offer was ₹95 LPA.
The Meta-Skill: Teaching Others to Use AI Effectively
There's one skill in this chapter that compounds more than any other: teaching your team to use AI effectively.
A single engineer with AI superpowers is a 2x engineer. An engineer who makes their entire 10-person team 2x more effective is a 20x engineer. That's the math that justifies ₹1Cr+ compensation.
The meta-skill has three components:
1. Prompt Crafting Workshops. Don't just share your prompts. Teach your team why they work. Run a 90-minute workshop. Show them a bad prompt, a good prompt, and the output difference. Let them feel the gap.
2. AI Code Review Standards. Create a team document: "How We Use AI in Code Review." Define what AI should catch (syntax errors, missing error handling, SQL injection) and what humans should catch (architecture decisions, trade-off judgments, business logic correctness). Make it explicit.
3. Agent Onboarding. When a new engineer joins, don't just give them access to the AI tools. Give them a 30-minute walkthrough of every agent your team has built. Show them the code review agent, the test generation agent, the incident response agent. Let them see the infrastructure. Make them feel like they've joined a team that's playing a different game.
Here's what that onboarding document looks like:
# AI-Native Engineering at [Company]
## Our Agents
- **PR Reviewer (`@pr-reviewer`)**: Runs on every PR. Catches bugs, security issues, and performance regressions. You still need to review — but focus on architecture and business logic, not syntax.
- **Test Generator (`@test-gen`)**: Generates test cases for new code. Run it before you write tests manually. It catches edge cases you'll miss.
- **Incident Responder (`@incident-bot`)**: First responder for production alerts. It diagnoses, mitigates, and escalates. You are the second responder.
- **Doc Writer (`@doc-bot`)**: Generates API docs from code. Run it after you merge. Review the output before publishing.
## Our Prompts
- All system prompts live in `prompts/`. They are version-controlled and regression-tested.
- To propose a prompt change: open a PR. CI runs the regression suite. If it passes, the change is reviewed like code.
- Never edit a prompt directly in production. You will break things.
## Our Rules
- AI-generated code must pass the same tests as human-written code.
- AI-generated code must be reviewed by a human before merging.
- If an agent produces wrong output, file a bug. We fix agents the same way we fix services.
- If you find a prompt that works well, share it. We all get better together.
This document takes 20 minutes to write and 30 minutes to walk through. It pays back in weeks.
Practice: Build Your First Agent System
You've read the theory. Now build something. This week.
Assignment: Build a PR Review Agent
- Fork a real repository — your team's codebase, or a popular open-source Node.js project.
- Implement the ReAct loop from this chapter. Use Claude's API or OpenAI's API.
- Give it three tools:
get_diff,read_file, andrun_tests. - Write a system prompt that produces structured, severity-ranked findings.
- Run it on 5 real PRs. Compare its output to human reviews.
- Measure: What percentage of its findings were valid? What did it miss? What did it flag that wasn't actually a problem?
- Iterate on the prompt and tools until the agent catches at least 50% of the bugs that human reviewers catch.
Stretch Goal: Add Self-Correction
- Implement the self-correcting loop. After the agent produces a review, have it evaluate its own output and improve it.
- Measure again. Does self-correction improve the quality? By how much?
Stretch Goal: Multi-Agent
- Split your single agent into three: a bug detector, a security auditor, and a performance reviewer.
- Build an orchestrator that runs all three and synthesizes their output.
- Compare the multi-agent output to the single-agent output. Which is better? In what ways?
This is not a toy exercise. This is the kind of project that becomes the centerpiece of a staff engineer promotion packet. Build it. Document it. Present it at your next engineering all-hands.
The engineers who will command ₹1 crore in 2027 are not the ones who write the most code. They are not the ones who work the most hours. They are the ones who understood, before everyone else, that the job changed.
The job is no longer to produce code. The job is to produce systems that produce code. The job is no longer to debug manually. The job is to build loops that debug themselves. The job is no longer to review PRs line by line. The job is to build agents that review a thousand PRs while you sleep.
You now know how to build those systems. The ReAct loop. The self-correcting loop. The multi-agent orchestrator. The CI/CD pipeline for AI. The self-healing infrastructure. The meta-skill of teaching others.
The gap between you and the ₹1 crore engineers is not knowledge. It's not talent. It's not years of experience. It's one thing: whether you build these systems, or whether you keep doing things the old way while the world moves past you.
Build the agent. Ship it. Show it to your manager. Show it to your team. Show it in your next interview.
The roles that will pay ₹1 crore in 2027 don't have titles yet. But they have a job description. And you just read it.