Autonomous Loops with Claude Code

Engineer /goal conditions that drive Claude Code to debug code autonomously.

Introduction

30 Second Summary

You have probably asked someone to "fix this" and gotten back something that technically met the request but completely missed the point. When you give an AI the same kind of vague instruction, the gap between what you meant and what it delivers is even wider.

In this project, you will engineer completion conditions for Claude Code's /goal command. You will start with a deliberately vague condition, watch it produce a false positive, then build a precise condition using a three-trait framework that drives autonomous debugging to completion.

What You'll Build

Picture typing a single /goal condition and watching Claude Code autonomously find and fix every bug across three modules, proving each fix by running your test suite right in the conversation.

By the end of this project, you'll have:

  • A JSON data transformer with six realistic bugs and a 12-test suite across three modules that defines what "correct" looks like. Six tests pass, six fail.
  • Firsthand proof of why vague conditions fail. You will have seen the /goal evaluator declare a false positive while bugs remained in the code, and you will know exactly which traits were missing from the condition.
  • A reusable three-trait framework (measurable end state, stated check, constraints) for writing /goal conditions that drive Claude Code through autonomous multi-turn work until verifiable completion.
  • Secret Mission: A non-interactive /goal workflow using claude -p that turns condition design into scriptable, headless automation.

Are there any prerequisites?

This project assumes you have Claude Code installed and configured, and Node.js on your machine. You should be comfortable with terminal commands and JavaScript ES module syntax.

Before We Start

Before you dive in, take a moment to lock in what you're about to build and why it matters.

Set Up the Buggy Project

Before you can engineer a completion condition, you need something to point it at. In this step, you will create a Node.js project with realistic behavioral bugs spread across three modules, plus a test suite that defines what correct behavior looks like.

The bugs are deliberately subtle. Silent data coercion, missing recursion, inverted logic, precision loss, missing validation. Running the tests gives you the starting point every /goal condition will work against: a clear count of failures to drive to zero.

In this step, get ready to:
  • Create a Node.js project and install Vitest as the test runner.
  • Create four source files with six realistic behavioral bugs planted across them.
  • Create a 12-test suite and run it to establish your 6-pass, 6-fail baseline.
Create the project and install Vitest

You need a project directory on your Desktop, a package.json configured for ES modules, and Vitest installed so the test suite can run.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir json-transformer
cd json-transformer
  • Create the project's subdirectories by running:
mkdir src tests
  • Confirm the folders exist by running:
ls

You should see src and tests listed in the output.

  • Open your code editor and create a new file called package.json in the json-transformer folder (the project root). Paste the following code:
{
  "name": "json-transformer",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "test": "vitest run"
  },
  "devDependencies": {
    "vitest": "4.1.10"
  }
}

What does this configure?

"type": "module" enables ES module syntax (import/export) across all .js files in the project.

The test script runs vitest run in single-run mode. This is important because watch mode would never exit, which would break any automated /goal condition that needs a clean pass/fail result.

  • Save package.json.
  • Install the project dependencies by running:
npm install

You should see output confirming that packages were installed. A node_modules folder and package-lock.json file now exist in your project.

Seeing an npm error?

Make sure you saved package.json before running npm install. If npm reports a JSON parse error, double check that the file content matches the code block above exactly.

Help me debug my npm install error.

Create the source files with planted bugs

The project has four source files in src/. Each one contains working code with realistic behavioral bugs baked in. You are not meant to find or fix these bugs yourself. The test suite will expose them, and your /goal conditions will drive Claude to fix them.

  • Create a new file at src/parser.js and paste the following code:
export function parseInput(raw) {
  let data;
  try {
    data = JSON.parse(raw);
  } catch (e) {
    throw new Error(`Invalid JSON: ${e.message}`);
  }

  // Bug: arrays are silently coerced to empty objects instead of throwing
  if (Array.isArray(data)) {
    return {};
  }

  // Bug: strips keys with null values, causing silent data loss
  const cleaned = {};
  for (const [key, value] of Object.entries(data)) {
    if (value !== null) {
      cleaned[key] = value;
    }
  }

  return cleaned;
}

What bugs are in the parser?

  • Array inputs are silently coerced to an empty object ({}) instead of throwing an error. Data goes in and vanishes.
  • Keys with null values are stripped during parsing. The caller never finds out their data was silently removed.
  • Save src/parser.js.
  • Create a new file at src/transformer.js. This file has four functions, so you will paste it in two parts. Start with the first two functions:
export function renameKeys(obj, renames) {
  const result = {};
  for (const [key, value] of Object.entries(obj)) {
    const newKey = renames[key] || key;
    // Bug: does not recurse into nested objects
    result[newKey] = value;
  }
  return result;
}

export function flatten(obj, prefix = '', separator = '.') {
  const result = {};
  for (const [key, value] of Object.entries(obj)) {
    const newKey = prefix ? `${prefix}${separator}${key}` : key;
    if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
      Object.assign(result, flatten(value, newKey, separator));
    } else {
      result[newKey] = value;
    }
  }
  return result;
}
  • Continue in the same file. Add the following two functions directly below the flatten function:

export function filterFields(obj, fields, mode = 'include') {
  const result = {};
  for (const [key, value] of Object.entries(obj)) {
    if (mode === 'include' && fields.includes(key)) {
      result[key] = value;
    } else if (mode === 'exclude' && fields.includes(key)) {
      // Bug: includes matching fields in exclude mode (logic inverted)
      result[key] = value;
    }
  }
  return result;
}

export function convertTypes(obj, conversions) {
  const result = { ...obj };
  for (const [key, targetType] of Object.entries(conversions)) {
    if (!(key in result)) continue;
    const val = result[key];
    if (targetType === 'number') {
      // Bug: parseInt loses decimal precision (should be parseFloat)
      result[key] = parseInt(val, 10);
    } else if (targetType === 'boolean') {
      result[key] = val === 'true';
    } else if (targetType === 'string') {
      result[key] = String(val);
    }
  }
  return result;
}

What bugs are in the transformer?

  • renameKeys only renames top-level keys. Nested objects pass through untouched because the function never recurses.
  • filterFields has inverted logic in exclude mode. It keeps the fields it should drop.
  • convertTypes uses parseInt instead of parseFloat for number conversion, so decimals like 19.99 become 19.
  • flatten is deliberately bug-free. Not every function has a bug, which makes the codebase more realistic.
  • Save src/transformer.js.
  • Confirm your first two source files exist by running:
ls src/

You should see parser.js and transformer.js listed.

  • Create a new file at src/validator.js and paste the first part of the function:
export function validateConfig(config) {
  const errors = [];

  if (config.rename) {
    // Bug: does not check for duplicate target names
    for (const [from, to] of Object.entries(config.rename)) {
      if (typeof to !== 'string' || to.trim() === '') {
        errors.push(`Invalid rename target for "${from}": must be a non-empty string`);
      }
    }
  }

  if (config.filter) {
    if (!Array.isArray(config.filter.fields)) {
      errors.push('filter.fields must be an array');
    }
    if (config.filter.mode && !['include', 'exclude'].includes(config.filter.mode)) {
      errors.push(`Invalid filter mode: "${config.filter.mode}"`);
    }
  }
  • Continue in the same file. Add the following code directly below the config.filter block:

  if (config.convert) {
    const validTypes = ['string', 'number', 'boolean'];
    for (const [key, type] of Object.entries(config.convert)) {
      if (!validTypes.includes(type)) {
        errors.push(`Invalid conversion type for "${key}": "${type}"`);
      }
    }
  }

  return { valid: errors.length === 0, errors };
}

What bug is in the validator?

validateConfig checks that rename targets are non-empty strings, but it never checks whether two different source keys rename to the same target. Two keys mapping to "name" would silently overwrite each other.

  • Save src/validator.js.
  • Create the last source file at src/index.js. This is the orchestrator that wires all modules together. Paste the imports and the start of the function:
import { readFileSync } from 'fs';
import { parseInput } from './parser.js';
import { renameKeys, flatten, filterFields, convertTypes } from './transformer.js';
import { validateConfig } from './validator.js';

export function transform(rawJson, config) {
  const validation = validateConfig(config);
  if (!validation.valid) {
    throw new Error(`Invalid config: ${validation.errors.join(', ')}`);
  }

  let data = parseInput(rawJson);
  • Continue in the same file. Add the transformation pipeline and closing brace directly below let data = parseInput(rawJson);:

  if (config.rename) {
    data = renameKeys(data, config.rename);
  }

  if (config.flatten) {
    data = flatten(data, '', config.flatten.separator || '.');
  }

  if (config.filter) {
    data = filterFields(data, config.filter.fields, config.filter.mode);
  }

  if (config.convert) {
    data = convertTypes(data, config.convert);
  }

  return data;
}

What does index.js do?

index.js itself is bug-free. It validates the config, parses the input, then applies each transformation step in order: rename, flatten, filter, convert. The bugs live in the individual modules it calls.

  • Save src/index.js.
  • Confirm all four source files exist by running:
ls src/

You should see index.js, parser.js, transformer.js, and validator.js listed.

Missing a file?

Make sure each file was created inside the src/ folder, not in the project root. If a file ended up in the wrong location, move it into src/ or recreate it there.

Help me find my missing source files.

Create the test suite and run the baseline

The test suite is split across three files that mirror the source modules. Each test defines what correct behavior looks like. Six tests will pass (the non-buggy behaviors) and six will fail (exposing each planted bug).

  • Create a new file at tests/parser.test.js and paste the following code:
import { describe, it, expect } from 'vitest';
import { parseInput } from '../src/parser.js';

describe('parseInput', () => {
  it('parses valid JSON objects', () => {
    const result = parseInput('{"name": "Alice", "age": 30}');
    expect(result).toEqual({ name: 'Alice', age: 30 });
  });

  it('throws on invalid JSON', () => {
    expect(() => parseInput('not json')).toThrow('Invalid JSON');
  });

  it('throws on array input instead of silently coercing', () => {
    expect(() => parseInput('[1, 2, 3]')).toThrow();
  });

  it('preserves keys with null values', () => {
    const result = parseInput('{"name": "Alice", "middleName": null, "age": 30}');
    expect(result).toEqual({ name: 'Alice', middleName: null, age: 30 });
  });
});

What do these tests check?

Four tests cover the parser. Two test normal behavior (valid JSON parsing, invalid JSON throwing). Two test the bugs: one expects array input to throw instead of being silently coerced, and one expects null values to survive parsing instead of being stripped.

  • Save tests/parser.test.js.
  • Create a new file at tests/transformer.test.js and paste the following code:
import { describe, it, expect } from 'vitest';
import { renameKeys, flatten, filterFields, convertTypes } from '../src/transformer.js';

describe('renameKeys', () => {
  it('renames top-level keys', () => {
    const result = renameKeys({ name: 'Alice', age: 30 }, { name: 'fullName' });
    expect(result).toEqual({ fullName: 'Alice', age: 30 });
  });

  it('renames keys in nested objects', () => {
    const input = { user: { name: 'Alice', age: 30 }, active: true };
    const result = renameKeys(input, { name: 'fullName' });
    expect(result.user.fullName).toBe('Alice');
    expect(result.user.name).toBeUndefined();
  });
});

describe('flatten', () => {
  it('flattens nested objects with dot separator', () => {
    const input = { user: { name: 'Alice', address: { city: 'NYC' } } };
    const result = flatten(input);
    expect(result).toEqual({
      'user.name': 'Alice',
      'user.address.city': 'NYC',
    });
  });
});

describe('filterFields', () => {
  it('excludes specified fields', () => {
    const input = { name: 'Alice', age: 30, email: 'alice@test.com' };
    const result = filterFields(input, ['age'], 'exclude');
    expect(result).toEqual({ name: 'Alice', email: 'alice@test.com' });
  });
});

describe('convertTypes', () => {
  it('converts string to number preserving decimals', () => {
    const result = convertTypes({ price: '19.99' }, { price: 'number' });
    expect(result.price).toBe(19.99);
  });
});

What do these tests check?

Five tests cover the transformer module. The top-level rename test and the flatten test will pass because those behaviors work correctly. The nested rename test, the exclude-mode filter test, and the decimal conversion test will each fail because they hit the three transformer bugs.

  • Save tests/transformer.test.js.
  • Confirm the first two test files exist by running:
ls tests/

You should see parser.test.js and transformer.test.js listed.

  • Create the last test file at tests/validator.test.js and paste the following code:
import { describe, it, expect } from 'vitest';
import { validateConfig } from '../src/validator.js';

describe('validateConfig', () => {
  it('accepts a valid config', () => {
    const config = {
      rename: { name: 'fullName' },
      filter: { fields: ['name'], mode: 'include' },
    };
    expect(validateConfig(config).valid).toBe(true);
  });

  it('rejects empty rename targets', () => {
    const config = { rename: { name: '' } };
    const result = validateConfig(config);
    expect(result.valid).toBe(false);
  });

  it('rejects duplicate rename targets', () => {
    const config = { rename: { firstName: 'name', lastName: 'name' } };
    const result = validateConfig(config);
    expect(result.valid).toBe(false);
    expect(result.errors.some(e => e.toLowerCase().includes('duplicate'))).toBe(true);
  });
});

What do these tests check?

Three tests cover the validator. The valid-config and empty-target tests will pass. The duplicate-target test will fail because validateConfig never checks whether two source keys rename to the same destination.

  • Save tests/validator.test.js.

All seven project files are in place. Before you run the tests, think about what you expect to see. You have 12 tests across 3 files, with 6 bugs planted in the source code. How many tests do you think will pass, and how many will fail?

  • Run the full test suite to establish your baseline:
npx vitest run

You should see output showing 6 failed tests and 6 passed tests across 3 test files. This is your baseline: a clear, measurable gap between what the code does and what correctness looks like.

Why does this ratio matter?

6 passing and 6 failing is your scoreboard. Every /goal condition you write in the next steps will try to drive that failure count to zero. The specific numbers (12 tests total, 6 failing) become the measurable end state you will bake into your completion conditions.

Not seeing 6 pass and 6 fail?

If all 12 tests fail, check that package.json contains "type": "module". Without it, the import statements in every file will fail.

If you see a "Cannot find module" error, double check that your test files use ../src/ in their import paths and that each source file is saved inside the src/ folder.

Help me debug my Vitest test results.

✔️ Awesome, I've got everything!

Great. Make sure all your files are saved. Your baseline is locked in: 6 passing, 6 failing.

ⓧ I'd like to double check the full code

Scroll up and confirm each file matches the code blocks above. You should have these files:

  • package.json in the project root with "type": "module" and the vitest dev dependency.
  • src/parser.js with the parseInput function (array coercion and null-stripping bugs).
  • src/transformer.js with four functions: renameKeys, flatten, filterFields, and convertTypes.
  • src/validator.js with the validateConfig function (missing duplicate-target check).
  • src/index.js with the transform orchestrator function.
  • tests/parser.test.js with 4 tests.
  • tests/transformer.test.js with 5 tests.
  • tests/validator.test.js with 3 tests.

If any file is missing or different, scroll up to its code block and paste the content again.

Your codebase is ready: 6 bugs hiding in plain sight, 12 tests drawing the line between broken and correct. Next up, you will write your first /goal condition to make Claude Code fix everything autonomously. The question is whether your condition is precise enough to actually work.

Run Your First /goal - The Loose Condition

Your buggy project is ready. You have 6 tests passing and 6 failing across three modules, giving you a clear baseline for every debugging session.

Your first instinct is probably to tell Claude to fix the failing tests. In this step, you will run exactly that condition as a /goal and watch what happens when the evaluator has nothing concrete to verify against.

In this step, get ready to:
  • Run a vague /goal condition with no constraints or verification requirements.
  • Read the evaluator's verdict and reason in the conversation transcript.
  • Compare the verdict against actual test results to observe the gap.
Set the vague /goal condition

The /goal command sets a completion condition that Claude Code works toward across multiple turns. After each turn, a separate evaluator model checks the conversation transcript and decides whether the condition has been met.

  • Open your terminal and make sure you are inside the json-transformer directory.
  • Start a Claude Code session by running this command:
claude

You should see the Claude Code prompt appear, ready for input.

Not seeing the Claude Code prompt?

Make sure Claude Code is installed and configured. Run claude --version to confirm the CLI is available. If the command is not found, revisit the Claude Code installation in your prerequisites.

Help me troubleshoot why Claude Code isn't starting in my terminal.

  • Set your first /goal condition by typing this into the Claude Code prompt:
/goal fix the failing tests in this project

Claude immediately starts working on the project. Watch closely as it processes the codebase.

What is the /goal evaluator?

The /goal evaluator is a separate model (Haiku by default) that runs after each turn. It reads the conversation transcript and judges whether the condition has been met, is not yet met, or is impossible.

The evaluator is blind. It cannot run code, open files, or verify anything independently. It only sees what Claude surfaces in the conversation text.

  • As Claude works, pay attention to what it does. Does it run npx vitest run and show the output in the conversation? Or does it edit files and describe what it changed?
  • If Claude asks for permission to edit files or run commands, approve the requests so it can continue.
  • Wait for the /goal to resolve. You will see a ◎ /goal active indicator while it is running.
Read the evaluator's verdict

The /goal has resolved and the evaluator has made its judgment. Now you need to see exactly what evidence it used.

  • Press Ctrl+O to toggle the transcript viewer.
  • Scroll through the transcript to find the evaluator's most recent verdict and its reason.
  • Note what evidence the evaluator cited. Was it test output showing 0 failures? Or was it Claude's narrative description of code changes?
  • Press q to exit the transcript viewer when you are done reading.

What should I look for in the verdict?

The evaluator's reason tells you exactly what it based its judgment on. If the reason references Claude's description of fixes ("Claude fixed the bugs in parser.js and transformer.js") rather than actual test output, the evaluator had no verifiable evidence. It accepted Claude's claims at face value.

Compare the verdict against reality

You have the evaluator's verdict. Now it is time to check whether reality agrees.

  • Open a separate terminal window. Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal and press Enter to open a new terminal.
  • Navigate to your project directory by running:
cd ~/Desktop/json-transformer

Before you run the tests, pause and think. Based on what you saw in the transcript, do you think all 12 tests pass now? Or do you think some bugs remain?

  • Check the actual test results by running:
npx vitest run

Compare the output against the evaluator's verdict. How many tests pass? How many fail?

Why does the verdict not match reality?

If bugs remain despite the evaluator judging the condition satisfied, you have observed the false positive. The evaluator judged "Met" based on what appeared in the transcript. Without a required command or expected output in the condition, the evaluator had no way to distinguish Claude's claims from actual evidence.

This is the core architectural insight: the evaluator is blind to the codebase. It can only verify what Claude surfaces in conversation. A condition without a stated check gives the evaluator nothing to verify against.

  • Write down the four traits that your vague condition was missing:
  • No measurable end state. "Fix the failing tests" has no number to verify against.
  • No stated check. The condition did not require Claude to run npx vitest run and show the output.
  • No constraints. Claude could have weakened the tests to make them pass instead of fixing the code.
  • No turn limit. The session could run indefinitely, consuming tokens without bound.

You have now seen the blind evaluator's biggest weakness firsthand. The four gaps you identified are exactly what you will close in the next step by engineering a strong completion condition.

Engineer a Strong Completion Condition

In Step 2, you saw the cost of a vague condition firsthand. The evaluator judged "Met" while bugs still lived in your code, because nothing in your condition forced Claude to produce verifiable evidence.

Now you will close every one of those gaps. Each trait in the strong condition you build exists because of a specific failure mode you already observed. By the end, the evaluator's verdict and npx vitest run will agree.

In this step, get ready to:
  • Clear the session and map each gap from Step 2 to the trait that closes it.
  • Compose and run a strong /goal condition with all four traits.
  • Verify that the evaluator's verdict matches actual test results.
Clear context and map each gap to its fix

The /clear command resets the Claude Code session so the new condition starts with a clean transcript. Before writing the condition, you need to connect each gap you identified in Step 2 to the specific trait that closes it.

  • Reset your Claude Code session by typing this command:
/clear

Your session context is now empty. Before composing the condition, map each gap to the trait that fixes it.

What are the four traits of a strong /goal condition?

Each trait exists because removing it re-opens a specific false-positive path.

  • Measurable end state: "all 12 tests pass, 0 failures." Not "fix the tests," which has no number the evaluator can verify against.
  • Stated check: "run npx vitest run and include the full output in the conversation." This forces transcript evidence the evaluator can actually read. Without it, Claude can describe fixes without proving them.
  • Constraints: "no files in the tests/ directory may be modified." This prevents Claude from making tests pass by weakening or deleting them.
  • Turn limit: "stop after 15 turns." This bounds runtime and token spend so the loop does not run indefinitely.
Set the strong /goal condition and watch the evaluator

Now combine all four traits into one condition. The /goal command accepts up to 4,000 characters, so you have plenty of room to be specific.

  • Before setting the goal, make sure auto mode is enabled so Claude can approve its own tool calls and work without waiting for your permission at each step. Press Shift+Tab to cycle through permission modes until the status bar shows auto mode on.

How do /goal and auto mode work together?

Auto mode approves tool calls within a single turn (file reads, edits, command runs). The /goal evaluator checks the condition after every turn and decides whether Claude should keep going. Together, they let Claude work across many turns without needing your approval at each step.

  • Set the strong /goal condition by typing this into Claude Code:
/goal All 12 tests pass with 0 failures when running npx vitest run. You must run npx vitest run and include the full output in the conversation. Do not modify any files in the tests/ directory. Stop after 15 turns.

What does each part of this condition do?

  • "All 12 tests pass with 0 failures" gives the evaluator a measurable number to check against transcript output.
  • "You must run npx vitest run and include the full output" forces Claude to produce evidence the evaluator can read, not just descriptions of code changes.
  • "Do not modify any files in the tests/ directory" constrains Claude to fixing source code, not weakening tests to make them pass.
  • "Stop after 15 turns" puts a ceiling on runtime and token spend.

Claude starts working immediately after you set the goal. Watch the evaluator verdicts appear after each turn.

  • Watch for "Not yet met" verdicts after each turn. Each verdict includes a reason that becomes guidance for Claude's next turn.
  • Notice how the evaluator checks your condition against the transcript. It looks for test output showing the pass count, confirms npx vitest run was actually run, and validates against the constraints.
  • Watch for the moment the evaluator judges "Met" because the test output in the transcript shows all 12 tests passing with 0 failures.

Check the evaluator's reasoning

While the goal is active, press Ctrl+O to view the transcript and see each evaluator verdict's reasoning. This shows you exactly what the evaluator cited when judging "Not yet met" or "Met."

Confirm the verdict matches reality

This is the moment of truth. In Step 2, the evaluator's "Met" was a false positive. This time, the strong condition should have forced Claude to produce real evidence before the evaluator judged success.

Before you run the tests yourself, think about this: do you expect the test results to match the evaluator's verdict this time? What is different about this condition that would prevent the false positive you saw in Step 2?

  • Open a new terminal tab and confirm the test results by running:
npx vitest run

You should see all 12 tests passing with 0 failures across 3 test files. The evaluator's verdict and reality now agree.

Still seeing test failures?

  • If some tests still fail, the /goal may have hit its 15-turn limit before fixing all bugs. Run /clear and set the same /goal condition again. Claude will pick up the remaining failures with fresh context.
  • If the evaluator judged "Met" but tests still fail, check the transcript to see whether Claude actually ran npx vitest run and showed the output. If it did not, the condition may have been entered with a typo. Re-enter it exactly as written above.
  • If the issue persists, help me debug why tests still fail after running my /goal condition.

Why each trait matters

Now reflect on the contrast with Step 2. For each trait, consider what specific false positive it prevents.

  • Without the measurable end state ("all 12 tests pass"), the evaluator could judge "Met" on partial progress. "Some tests fixed" would be enough.
  • Without the stated check ("run npx vitest run"), the evaluator could judge "Met" based on Claude's description of code changes instead of actual test output.
  • Without the constraints ("don't modify tests/"), the evaluator could judge "Met" after Claude weakened or deleted failing tests.
  • Without the turn limit ("stop after 15 turns"), Claude could run indefinitely, burning tokens without converging.

This is the three-trait framework (measurable end state, stated check, constraints) plus a turn limit. You will use this for every /goal condition going forward.

Your three-trait framework is now battle-tested. Next up, you will add edge-case tests that expose deeper bugs and author a /goal condition entirely from scratch.

Add Edge Cases and Author Your Own Condition

In Step 3, you engineered a strong /goal condition that closed every gap the vague condition left open. The evaluator's verdict and reality finally agreed because your condition gave the evaluator something concrete to check against.

But following a framework someone else designed is one thing. Writing conditions from scratch for new problems is the real skill. In this step, you will add edge-case tests that probe whether Claude's earlier fixes were truly thorough, then author your own /goal condition to drive Claude through fixing whatever those tests expose.

In this step, get ready to:
  • Add two edge-case tests that catch shallow recursion fixes.
  • Author a /goal condition from scratch using the three-trait framework.
  • Verify all 14 tests pass with 0 failures.
Add edge-case tests that probe for shallow fixes

Claude fixed the renameKeys function in Step 3 so the existing nested-object test passed. But that test only checks one level of nesting. A common shortcut is to add recursion that goes one level deep but no further, or to recurse naively into arrays and corrupt them.

  • In tests/transformer.test.js, find the describe('renameKeys') block.
  • Add these two tests after the existing 'renames keys in nested objects' test, inside the same describe block:
  it('renames keys in deeply nested objects (3+ levels)', () => {
    const input = { level1: { level2: { level3: { name: 'deep' } } } };
    const result = renameKeys(input, { name: 'label' });
    expect(result.level1.level2.level3.label).toBe('deep');
    expect(result.level1.level2.level3.name).toBeUndefined();
  });

  it('preserves arrays inside nested objects during rename', () => {
    const input = { user: { name: 'Alice', tags: ['admin', 'active'] } };
    const result = renameKeys(input, { name: 'fullName' });
    expect(result.user.fullName).toBe('Alice');
    expect(result.user.tags).toEqual(['admin', 'active']);
  });

What do these tests catch?

The first test nests objects three levels deep. If Claude's fix only recursed one level, the name key at level 3 stays unchanged and the test fails.

The second test puts an array (tags) inside a nested object. If the recursion naively enters arrays, it corrupts them. The test checks that the array comes out exactly as it went in.

  • Save tests/transformer.test.js.
  • Run the tests in a separate terminal (not your Claude Code session) to see how Claude's earlier fixes hold up:
npx vitest run

Check the output against one of these two scenarios.

✔️ All 14 tests pass

Claude's Step 3 fix was thorough. The recursion handles deep nesting and skips arrays correctly. Your edge cases confirmed it. You will still author a /goal condition from scratch in the next substep for the practice.

ⓧ Some of the new tests fail

Your edge cases caught a shallow fix. Claude passed the existing nested test by adding just enough recursion, but the deeper probe exposed the gap. This is exactly why edge-case tests exist.

Do not fix the code yourself. You will author a /goal condition that makes Claude fix it autonomously.

Don't see 14 tests listed?

Make sure you added the two new tests inside the existing describe('renameKeys') block, not outside it. The closing }); of the describe block should come after your new tests.

Check for missing commas or unmatched brackets. A syntax error in the test file prevents all tests in that file from running.

Why are my new tests not showing up when I run Vitest?

✔️ Awesome, I've got everything!

Great. Make sure you have saved tests/transformer.test.js.

ⓧ I'd like to double check the full code

Your tests/transformer.test.js should now contain these describe blocks, in order:

  • describe('renameKeys') with 4 tests: 'renames top-level keys', 'renames keys in nested objects', 'renames keys in deeply nested objects (3+ levels)', and 'preserves arrays inside nested objects during rename'.
  • describe('flatten') with 1 test: 'flattens nested objects with dot separator'.
  • describe('filterFields') with 1 test: 'excludes specified fields'.
  • describe('convertTypes') with 1 test: 'converts string to number preserving decimals'.

Scroll up and confirm each test block you added is present and matches the code above.

Author your own /goal condition from scratch

You have the three-trait framework from Step 3. Now apply it to a new problem without anyone handing you the condition text.

What should your condition include?

Think through each trait before you type anything. Your condition needs four things:

  • Measurable end state - a number the evaluator can verify (how many tests, how many failures).
  • Stated check - which command Claude must run so the output appears in the transcript.
  • Constraints - which files Claude is NOT allowed to modify.
  • Turn limit - a cap on how many turns Claude gets before stopping.

Compose your condition, then try setting it yourself.

✔️ I'll give it a go!

Write out your condition on paper or in a scratch file first. Read it back as if you were the evaluator. Could you judge "Met" or "Not yet met" based only on what you see in the conversation transcript? If yes, it is ready.

  • Switch back to your Claude Code session.
  • Type /goal followed by your condition and press Enter.

ⓧ Maybe a little help...

Here is an example condition that applies all four traits. Yours does not need to match word for word, but it should cover the same ground:

/goal All 14 tests pass with 0 failures when running npx vitest run. You must run npx vitest run and the complete output must appear in this conversation. Only files in the src/ directory may be modified. No files in the tests/ directory may be changed. Stop after 15 turns.

Notice how each sentence maps to one trait. The evaluator can verify every claim by reading the transcript.

  • Switch back to your Claude Code session.
  • Type /goal followed by your condition (or the example above) and press Enter.
Watch the autonomous debugging loop and verify

Claude starts working immediately after you set the goal. The evaluator checks your condition after every turn.

  • Watch the ◎ /goal active indicator as Claude works. Each turn, the evaluator reads the transcript and judges "Not yet met" or "Met."
  • Pay attention to whether Claude runs npx vitest run after each code change. Your stated check trait should force this behavior.

What if the loop stalls or resolves too early?

If the evaluator judges "Met" but tests still fail, your condition is missing specificity. Run /goal clear, tighten the condition, and try again. Each premature "Met" tells you which trait was too loose.

If Claude keeps answering without making progress (no tool use for several turns), Claude Code stops the loop and returns control to you with the goal still set. Adjust your condition or give Claude a nudge, then let it continue.

Once the evaluator judges "Met," it is time to verify that reality matches the verdict.

Before you run the next command, predict: do you expect all 14 tests to pass this time, or will there be a gap between the evaluator's verdict and reality like in Step 2?

  • Confirm the result by running this command in a separate terminal:
npx vitest run

You should see all 14 tests passing across 3 test files with 0 failures. The evaluator's verdict and reality agree because your condition gave the evaluator concrete, verifiable evidence to check against.

Still seeing failures after the goal resolved?

This means your condition had a gap. Check which trait was missing. Did you require Claude to run npx vitest run and show the output? Did you specify the exact pass count (14 tests, 0 failures)? Did you constrain test file modifications?

Run /goal clear to remove the resolved goal. Revise your condition to close the gap and set it again.

My /goal resolved as Met but tests still fail. How do I diagnose which trait my condition is missing?

Secret mission

Non-Interactive /goal from a Script

Interactive /goal taught you condition design. Now make it scriptable. Compose a complete /goal workflow as a single terminal command using claude -p, and monitor it through streamed JSON output. The same three-trait framework applies, but this time Claude works without you watching.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally, so there are no ongoing costs or cloud resources to worry about. Decide whether to keep your project files on your Desktop, or delete them entirely.

Resources you used:

  • The json-transformer project directory on your Desktop, containing source files, test files, and node_modules.

Keep everything running

No action needed. Choose this if you want to keep experimenting with /goal conditions or extend the test suite further.

  • Your json-transformer folder stays on your Desktop with all source files, tests, and node_modules intact.
  • You can return anytime and run npx vitest run to confirm all 15 tests still pass.

Pause - I'll come back to this later

There are no running processes or servers to stop. Everything in this project runs as one-shot commands.

  • Your project files stay on your Desktop. No background processes are consuming memory or CPU.
  • When you come back, open a terminal, navigate to the json-transformer folder on your Desktop, and pick up where you left off.

Delete - I don't want to use this again

Remove the entire project directory from your Desktop. This deletes all source files, test files, and the node_modules folder.

  • Delete the project folder by running the command for your operating system.

macOS

rm -rf ~/Desktop/json-transformer

Windows

rmdir /s /q %USERPROFILE%\Desktop\json-transformer
  • Confirm the folder is gone by running:
ls ~/Desktop

You should no longer see json-transformer in the output.

Nice Work!

Nice Work!

Nice work! You just engineered /goal conditions that reliably drive Claude Code through autonomous multi-turn debugging, and you understand exactly why each trait makes the difference between a false positive and a verified result.

You learned how to:

  • Run a deliberately vague /goal condition and observe the false positive firsthand: the blind evaluator judged "Met" without verifiable transcript evidence, while bugs remained in the code.
  • Engineer a strong /goal condition using the three-trait framework (measurable end state, stated check, constraints) and articulate why each trait prevents a specific false-positive failure mode.
  • Author a custom completion condition from scratch for new edge-case tests, demonstrating transferable loop engineering skill.
  • Secret Mission: Applied the same three-trait condition framework in a non-interactive claude -p workflow, turning loop engineering into scriptable infrastructure.

Ready to quiz yourself?