---
title: "Chapter 03: Project Scaffolding"
description: "Convert the bootstrap script into a structured TypeScript project with tests, linting, and modular code."
type: lesson
order: 3
chapter: "01-setup"
---

# Chapter 03: Project Scaffolding

## What We're Building

We'll take the single-file bootstrap script and expand it into a clean, modular TypeScript project. We'll add testing, linting, and a proper build system — all using our agent to do the work.

## Step 1: Scaffold the Project

Start your bootstrap agent and paste this prompt:

> Take bootstrap.ts and convert it into a cleanly structured typescript project. Use mise, bun and biome to control the building, running, testing, and linting of the project. Split out the prompt so that we can iterate on it going forward. Write tests for everything first, and make sure everything passes.

The agent will read your `bootstrap.ts`, understand the structure, and create:

```
src/
├── index.ts           # Entry point with REPL
├── agent.ts           # Core runTurn loop
├── lib/
│   ├── api.ts         # OpenRouter client
│   ├── types.ts       # Message and response types
│   └── prompts.ts     # System prompt (initially inline)
├── tools/
│   └── index.ts       # Tool definitions and dispatch
tests/
├── sanity.test.ts     # Basic tests
mise.toml              # Updated with test/check tasks
biome.json             # Linter configuration
```

## Step 2: Add Testing and Linting

Now paste this follow-up prompt:

> run the linters and the tests to make sure everything passes. make sure you have a test for the bash tool, make sure it can list all of the files in the directory

The agent will:

1. Run `biome check` to lint the code
2. Run `bun test` to execute the test suite
3. Add a test specifically for the `bash` tool that verifies it can list files

Your `mise.toml` should end up with tasks like:

```toml
[tasks]
check = "biome check && bun test"
test = "bun test"
lint = "biome check"
```

### The Check Command

Throughout this course, we'll use `mise run check` as our verification step. It runs both the linter and tests. The agent's `run_check` tool wraps exactly this command.

## Step 3: Split the System Prompt

The system prompt should be in its own file so we can iterate on it. Your agent will create something like:

`src/prompts/default.md`:

```markdown
---
name: default
description: The default full-featured agent.
tools:
  [
    bash,
    list_files,
    read_file,
    write_file,
    replace_in_file,
    search_files,
    run_check,
    git_diff,
    download_url,
  ]
subagents: [feature-planner, tech-researcher, code-map, poet]
---

You are a helpful AI assistant capable of running bash commands and editing files on the local system.
You have access to a set of tools to navigate, read, edit, and verify code.

# Workflow Rules

1. **Explore First**: Don't guess file paths. List and read files to understand the context.
2. **Verify Your Work**: After creating or modifying files, ALWAYS run "run_check" to verify.
3. **Atomic Changes**: Make small changes and verify them.
4. **Fix Issues**: If 'run_check' fails, analyze the error and fix the code immediately.
5. **Clean Code**: Maintain clean, type-safe code.
6. **No Auto-Commit**: NEVER automatically commit changes. Ask the user first.
```

The YAML frontmatter (between `---` markers) defines:

- **name**: The agent's identifier
- **description**: When to use this prompt
- **tools**: Which tools this agent can access
- **subagents**: Which other agents this one can spawn

## Step 4: Test the New Agent

Run your new structured agent:

```bash
bun run src/index.ts
```

Try a few commands to make sure everything works:

```
what files are in the src directory?
```

```
run the tests for me
```

```
check the code for linting issues
```

If things aren't working, go back to your `bootstrap.ts` agent and paste the errors — it will help debug.

### The Bootstrap-as-Debugger Pattern

This is a key workflow you'll use throughout the course: **when your new agent breaks, use your old agent to fix it.** The bootstrap agent is simpler and more reliable — use it as a debugging tool.

1. Copy the error from the broken agent
2. Paste it into the bootstrap agent: "Here's an error I got. What's wrong and how do I fix it?"
3. Apply the fix
4. Test again

## Step 5: Verify Everything

```bash
mise run check
```

You should see:

```
Checked 12 files in 45ms. No errors.
✓ sanity.test.ts: all tests pass
✓ tools.test.ts: bash tool lists files correctly
✓ tools.test.ts: bash tool handles errors gracefully
```

## What We Have Now

```
weekend-coding-agent/
├── src/
│   ├── index.ts           # REPL entry point
│   ├── agent.ts           # Core runTurn loop
│   ├── lib/
│   │   ├── api.ts         # OpenRouter client
│   │   ├── types.ts       # Type definitions
│   │   └── prompts.ts     # Prompt loading
│   ├── tools/
│   │   └── index.ts       # Tool dispatch
│   └── prompts/
│       └── default.md     # System prompt
├── tests/
│   ├── sanity.test.ts     # Core tests
│   └── tools.test.ts      # Tool tests
├── .env
├── .gitignore
├── mise.toml              # Tasks and dependencies
├── biome.json             # Linter config
└── package.json           # Bun project config
```

## Key Source Files

Here's the project structure we're building toward. The `runTurn` function in `src/agent.ts` is the heart of the agent:

```typescript
// src/agent.ts — The core loop
export async function runTurn(
  history: Message[],
  apiCaller: ApiCaller = callLLM,
  toolExecutor: CommandExecutor = defaultBashExecutor,
  tools: any[] = TOOLS,
  logger?: Logger,
  agentName = "default",
): Promise<TurnResult> {
  const currentMessages = [...history];
  const usage = { prompt_tokens: 0, completion_tokens: 0 };

  while (true) {
    const response = await apiCaller(currentMessages, tools);
    // accumulate usage tokens...
    const m = response.choices[0].message;

    if (m.tool_calls && m.tool_calls.length > 0) {
      // Execute ALL tool calls in parallel
      const toolResults = await Promise.all(
        m.tool_calls.map(async (toolCall) => {
          let result = await executeTool(
            toolCall.function.name,
            JSON.parse(toolCall.function.arguments),
          );
          return { role: "tool", tool_call_id: toolCall.id, content: result };
        }),
      );
      currentMessages.push(...toolResults);
      // Loop continues to feed tool results back to LLM
    } else {
      // Final response — done
      currentMessages.push({ role: "assistant", content: m.content });
      break;
    }
  }

  return { messages: currentMessages, usage };
}
```

The types in `src/lib/types.ts`:

```typescript
export interface Message {
  role: "system" | "user" | "assistant" | "tool";
  content?: string;
  tool_calls?: any[];
  tool_call_id?: string;
  reasoning_details?: any;
  name?: string;
}

export interface CompletionResponse {
  choices: { message: Message }[];
  usage?: {
    prompt_tokens: number;
    completion_tokens: number;
    total_tokens: number;
  };
  error?: { message: string };
}
```

---

[← Chapter 02](/build-ai-coding-agent/01-setup/02-bootstrap/) · [Next: Part 2 →](/build-ai-coding-agent/02-core/)
