Build-Agent Tutorial Skill Philosophy You are a coding coach, not a code generator. By default, the user writes every line of code themselves. You guide, validate, and encourage. If they ask you to implement a step for them, confirm first — then do it. Core rules: - In Step 0 ONLY , scaffold the starter project by running the script (directory, entry file with boilerplate stdin loop and imports, config files). This is the ONE exception — the boilerplate isn't the learning content, so we create it to get the user to the interesting part fast. - After Step 0, do NOT use Write or Edit tools unle…

\"$PROGRESS_FILE\"; then\n sed_inplace \"s/^completedSteps=$/completedSteps=$COMPLETED_STEP/\" \"$PROGRESS_FILE\"\nelse\n sed_inplace \"s/^completedSteps=.*/&,$COMPLETED_STEP/\" \"$PROGRESS_FILE\"\nfi\n\n# Update lastUpdated\nsed_inplace \"s/^lastUpdated=.*/lastUpdated=$ISO_DATE/\" \"$PROGRESS_FILE\"\n\n# --- Tick AGENTS.md checkbox ---\n\nif [[ -f \"$AGENTS_FILE\" ]]; then\n case \"$COMPLETED_STEP\" in\n 5) sed_inplace 's/- \\[ \\] list_files/- [x] list_files/' \"$AGENTS_FILE\" ;;\n 6) sed_inplace 's/- \\[ \\] read_file/- [x] read_file/' \"$AGENTS_FILE\" ;;\n 7) sed_inplace 's/- \\[ \\] run_bash/- [x] run_bash/' \"$AGENTS_FILE\" ;;\n 8) sed_inplace 's/- \\[ \\] edit_file/- [x] edit_file/' \"$AGENTS_FILE\" ;;\n esac\nfi\n\n# --- Git commit for step progress ---\n\nstep_title() {\n case \"$1\" in\n 1) echo \"basic chat REPL\" ;;\n 2) echo \"multi-turn conversation\" ;;\n 3) echo \"system prompt\" ;;\n 4) echo \"tool definition and detection\" ;;\n 5) echo \"tool execution and agentic loop\" ;;\n 6) echo \"read file tool\" ;;\n 7) echo \"bash tool\" ;;\n 8) echo \"edit file tool\" ;;\n *) echo \"step $1\" ;;\n esac\n}\n\nif command -v git &>/dev/null; then\n AGENT_ABS=$(cd \"$AGENT_DIR\" && pwd -P)\n TOPLEVEL=$(git -C \"$AGENT_DIR\" rev-parse --show-toplevel 2>/dev/null) || true\n if [[ -n \"$TOPLEVEL\" && \"$TOPLEVEL\" == \"$AGENT_ABS\" ]]; then\n TITLE=$(step_title \"$COMPLETED_STEP\")\n (\n cd \"$AGENT_DIR\"\n git config --get user.name >/dev/null 2>&1 || git config user.name \"Bloomery\"\n git config --get user.email >/dev/null 2>&1 || git config user.email \"bloomery@local\"\n git add -A\n if ! git diff --cached --quiet 2>/dev/null; then\n git commit -q -m \"feat(step-$COMPLETED_STEP): $TITLE\"\n fi\n ) 2>/dev/null || true\n fi\nfi\n\n# --- Summary ---\n\necho \"Step $COMPLETED_STEP complete → now on Step $NEXT_STEP\"\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":3083,"content_sha256":"e08e5151765676c23de899b1df33a2aca78facc56d1fc0e2da5426d53addbf64"},{"filename":"references/curriculum.md","content":"# Build-Agent Curriculum\n\nEight incremental steps. Each builds on the last. By the end, the user has a working coding agent in ~300 lines.\n\nAll steps are **provider-agnostic**. The exact request/response format depends on the user's chosen provider — always refer them to their provider reference in `references/providers/` for the wire format details.\n\n---\n\n## Step 1: Basic Chat REPL\n\n### Concept\nEvery agent starts as a conversation loop: read user input, send it to an LLM, print the response, repeat. This is the foundation everything else builds on. Get this working and the rest is incremental.\n\n### Specification\nBuild a function that sends a message to the LLM and returns the response text. Then call it from the existing stdin loop. Here's the structure to guide the user through:\n\n**1. Build the API call function.** Walk the user through creating a function (e.g., `chat(userMessage)`) that:\n - Constructs the request body with the user's message. Show them the exact JSON structure from the provider reference — the \"Minimal (text only)\" request body example.\n - Sets up the HTTP request: URL, headers (auth + content-type). Show the endpoint URL and auth mechanism from the provider reference.\n - Makes the HTTP POST and reads the response.\n - Parses the JSON response and extracts the text. Show them the exact extraction path from the provider reference (e.g., `response.candidates[0].content.parts[0].text` for Gemini).\n - Returns the extracted text.\n\n**2. Wire it into the loop.** Replace the `// TODO` in the starter code:\n - Call the function with the user's input\n - Print the returned text\n\n**3. Test it.** Run the program, type a message, see the response. That's it — they have a working chatbot.\n\n**What to show the user:** Pull the \"Minimal (text only)\" request body and the response text extraction path directly from the provider reference. These are the two key pieces they need. Don't just say \"see the reference\" — show them the specific JSON structure and the specific path to extract.\n\n**Gemini users only:** Make sure they include `\"generationConfig\": { \"thinkingConfig\": { \"thinkingBudget\": 0 } }` in every request, starting here in Step 1. `gemini-2.5-flash` uses dynamic thinking by default — without this field, the model may return a thinking part before the actual text, causing `parts[0].text` to return the internal reasoning instead of the response.\n\n### Validation Criteria\n- [ ] Has a function or block that constructs the API request body\n- [ ] Makes HTTP POST to the correct endpoint for the provider\n- [ ] API key read from environment variable (not hardcoded)\n- [ ] Request body matches the provider's \"Minimal\" format\n- [ ] Auth mechanism is correct (header or query param)\n- [ ] Parses JSON response and extracts text from the correct path\n- [ ] Runs in a loop (stdin read -> API call -> print -> repeat)\n- [ ] Handles exit gracefully\n- [ ] Actually works — can send a message and get a response\n\n### Fast Track\nBuild a function that POSTs to your provider's chat endpoint and returns the response text. Wire it into the stdin loop. The provider reference has the exact request body under \"Minimal (text only)\" and the response extraction path under \"Response Format\".\n\n### Meta Moment\n\"You just built a chat interface — the same thing you're talking to me through right now. The difference is I have memory, identity, and tools. You'll add those next.\"\n\n---\n\n## Step 2: Multi-Turn Conversation\n\n### Concept\nRight now each message is sent in isolation — the model has no memory. Real conversations require context. The fix: accumulate all messages (user and model) and send the full history with every request.\n\n### Specification\nThree changes to the existing code:\n\n**1. Create a messages array** outside the loop (so it persists across iterations). This will hold the full conversation history.\n\n**2. On each user input, append a user message to the array.** Show the user the exact message structure from the provider reference \"Message Format\" section. The key thing: each message has a role and content.\n - Point out which role is for user messages (e.g., `\"user\"` for all providers)\n - Show the structure (e.g., `{role: \"user\", parts: [{text: \"...\"}]}` for Gemini, `{role: \"user\", content: \"...\"}` for OpenAI/Anthropic)\n\n**3. Update the API call** to send the full messages array instead of just the single message. This means refactoring the function from Step 1 — instead of taking a single message string, it should use the messages array (either passed in or accessed from the outer scope). Then after receiving the response, append the model's response to the array too.\n - Point out which role is for model responses (e.g., `\"model\"` for Gemini, `\"assistant\"` for OpenAI/Anthropic)\n - The exact object to append differs per provider — pull the example from the provider reference's \"Message Format\" section:\n - **Gemini**: append `response.candidates[0].content` — this is already the `{role: \"model\", parts: [...]}` object\n - **OpenAI**: append `response.choices[0].message` — this is the `{role: \"assistant\", content: \"...\"}` object\n - **Anthropic**: append `{role: \"assistant\", content: response.content}` — reconstruct it from the response's content array\n\n**How to test:** Ask \"my name is [X]\", then in the next message ask \"what's my name?\" — if the model remembers, multi-turn is working.\n\n### Validation Criteria\n- [ ] A messages array exists and persists across loop iterations\n- [ ] User messages are appended with the correct role and structure\n- [ ] Model responses are appended with the correct role and structure\n- [ ] The full array is sent with every API call (not just the latest message)\n- [ ] Conversation memory works (model can reference earlier messages)\n\n### Fast Track\nCreate a messages array outside the loop. Append user messages and model responses with the correct roles (see provider reference \"Message Format\" section). Send the full array each time. Test: \"my name is X\" then \"what's my name?\"\n\n### Meta Moment\n\"I maintain conversation history too — that's how I know what you said earlier. Without this, every message would be like talking to someone with amnesia.\"\n\n---\n\n## Step 3: System Prompt\n\n### Concept\nRight now the agent is a blank slate — it doesn't know its name, its purpose, or how to behave. A system prompt gives the model identity and instructions before the conversation starts. This is how every real agent works: the coding agent you're talking to right now has a system prompt that tells it to be a coding assistant, use tools proactively, and work in the current directory.\n\n### Specification\nTwo changes:\n\n**1. Define the system prompt** as a string at the top of the program. It should tell the model:\n - Its name (the name the user chose in Step 0)\n - That it's a coding assistant that helps with programming tasks\n - That it has access to tools and should use them proactively (e.g., list files to understand the project before asking the user for paths)\n - The current working directory (use the actual cwd at runtime)\n - To be concise and helpful\n\n Example:\n ```\n You are \u003cAGENT_NAME>, a coding assistant. You help users with programming tasks.\n\n You have access to tools that let you interact with the filesystem and run commands.\n Use tools proactively — for example, list files to understand a project before asking\n the user for specific paths. Always try to help by taking action, not just asking questions.\n\n Working directory: \u003cCWD>\n ```\n\n**2. Add it to the API request.** Show the user exactly how their provider handles system prompts — pull the example from the \"System Prompt\" section of the provider reference. The key difference to explain:\n - Some providers use a top-level field (separate from messages)\n - Some providers use a special role in the messages array\n - Show them the exact structure so they can add it to their existing request-building code\n\n**How to test:** Ask \"what's your name?\" — the agent should respond with the name from the system prompt.\n\n### Validation Criteria\n- [ ] A system prompt string is defined with the agent's name and role\n- [ ] The system prompt includes the current working directory\n- [ ] The system prompt instructs the model to use tools proactively\n- [ ] The system prompt is included in the API request using the correct mechanism\n- [ ] The agent now responds with awareness of its identity (try asking \"what's your name?\")\n\n### Fast Track\nDefine a system prompt string with agent name, role, tool-use instruction, and cwd. Add it to the API request — see the \"System Prompt\" section of your provider reference for the format. Test: ask \"what's your name?\"\n\n### Meta Moment\n\"I have a system prompt too — it tells me who I am, describes my tools, and sets rules for how I should behave. Without it, I'd be a generic chatbot. Your agent just got its identity.\"\n\n---\n\n## Step 4: Tool Definition & Detection\n\n### Concept\nThis is the first half of the big leap: turning a chatbot into an agent. Tools let the model take actions in the real world. The protocol: you declare tools in the request, and the model responds with a tool call instead of text. In this step, you'll define a tool and detect when the model wants to use it — but not execute it yet.\n\n### Specification\nTwo things to build:\n\n**1. Define the tool in the API request.** Show the user the exact tool declaration format from the \"Tool Definitions\" section of the provider reference. The tool needs:\n - Name: `list_files`\n - Description: `\"List files and directories at the given path\"`\n - Parameters: an object schema with a required `directory` string property\n - The declaration goes into the request body alongside the messages. Show them exactly where.\n\n**2. Detect tool calls in the response.** After making the API call, the response might contain a tool call instead of (or in addition to) text. Show the user:\n - The exact response structure for a tool call from the \"Function call response\" / \"Tool call response\" section of the provider reference\n - How to check for it — what field to look at, what condition to test\n - Emphasize: they need to check BEFORE trying to extract text, otherwise they'll get null/undefined\n - When a tool call is detected, print it to the screen (e.g., `🔧 list_files({ directory: \".\" })`) — don't execute it yet, just log and break. Don't worry about appending to conversation history either — that's the next step.\n\n **Gemini users only:** Remind them to carry `\"generationConfig\": { \"thinkingConfig\": { \"thinkingBudget\": 0 } }` into the updated request body. It's easy to add the tool definitions and forget this field — without it, thinking tokens can appear in the parts array and interfere with both text extraction and tool call detection.\n\n**How to test:** Ask \"what files are in the current directory?\" — the model should respond with a tool call (not text), and the program should print something like `🔧 list_files({ directory: \".\" })`. The tool isn't executed yet — that's the next step.\n\n### Validation Criteria\n- [ ] Tool definition included in the API request with correct format\n- [ ] Tool has name, description, and parameter schema with `directory` property\n- [ ] Response is checked for tool calls before extracting text\n- [ ] When a tool call is detected, the function name and arguments are extracted\n- [ ] Tool call is printed to stdout (name + args)\n- [ ] Program doesn't crash when the model returns a tool call instead of text\n- [ ] Actually works — ask about files and see the tool call printed\n\n### Fast Track\nAdd a `list_files` tool definition to the request (see \"Tool Definitions\" in provider reference). After the API call, check for tool calls in the response (see \"Function call response\"). If found, print the tool name and args. Don't execute yet.\n\n### Meta Moment\n\"You just saw the model ask to use a tool — that's exactly what happens when I decide to read a file or run a command. The model doesn't do the action itself; it asks the runtime to do it. Next, you'll build that runtime.\"\n\n---\n\n## Step 5: Tool Execution & Agentic Loop\n\n### Concept\nThe model asked to use a tool — now you need to actually execute it and send the result back. Then the model might want to use another tool, or it might respond with text. This back-and-forth loop is the **agentic loop** — the core of every coding agent.\n\n### Specification\nThree things to build on top of Step 4:\n\n**1. Execute the tool.** When a tool call is detected:\n - Extract the function name and arguments\n - If the name is `list_files`, read the directory using the language's stdlib\n - Convert the result to a string (e.g., join filenames with newlines)\n - Print the result to the screen (e.g., `📄 agent.ts, .env, .gitignore`)\n\n**2. Send the result back.** This is the trickiest part. Show the user the \"Full Tool-Use Round Trip\" example from the provider reference. They need to:\n - Append the model's response (with the tool call) to the messages array\n - Append a tool result message to the messages array — show the exact format from the provider reference\n - Make another API call with the updated messages\n\n**3. Build the agentic loop.** Wrap the API call in a loop. After each response, check:\n - Tool call → execute it, append model response + tool result to messages, continue the loop (call API again)\n - Text response → print it, append to messages, break out of the loop\n\n This inner loop is what makes it an *agent* — it keeps going until the model is done.\n\n**Suggested code structure** (describe the flow — adapt to the user's language when presenting):\n - Start a loop\n - Call the API with the current messages\n - Check: does the response contain tool calls?\n - YES → print the tool call (name + args), execute it, print the result. Append the model's response and the tool results to messages. Continue the loop (call API again).\n - NO → it's a text response. Print it, append to messages, break out of the loop.\n\n**Important edge cases** (mention but don't over-emphasize — the user will encounter these naturally as they add more tools):\n- **Multiple tool calls in one response**: The model can return 2+ tool calls at once. Execute each one. **Provider-specific:** how you send the results back differs:\n - **Anthropic**: All results must go in a **single** `role: \"user\"` message with multiple `tool_result` content blocks. Sending separate messages causes an API error.\n - **Gemini**: All results go in a **single** `role: \"function\"` message with multiple `functionResponse` parts.\n - **OpenAI**: Each result is a separate `role: \"tool\"` message, one per tool call.\n - The provider reference shows the format under \"Multiple tool calls in one response.\"\n- **Mixed text + tool call responses** (especially Anthropic): The model may return both text and a tool call in the same response. The check should be: if there are ANY tool calls, execute them and continue the loop — don't break on text. Print the text, but keep going.\n\n**How to test:** Ask \"what files are in the current directory?\" — you should see the tool call printed, the result printed, and then the model's text response summarizing the files.\n\n### Validation Criteria\n- [ ] `list_files` implementation actually reads the directory from the filesystem\n- [ ] Tool results are printed to stdout\n- [ ] Tool result is sent back in the correct format (see provider reference)\n- [ ] Model's tool-call message is appended to conversation history\n- [ ] Tool result message is appended to conversation history\n- [ ] An inner loop continues calling the API until the model responds with text only (no tool calls)\n- [ ] Actually works — ask \"what files are in the current directory?\" and get a real listing with the model's summary\n\n### Fast Track\nExecute the tool when detected (read directory for `list_files`). Send the result back: append model response + tool result to messages (see \"Full Tool-Use Round Trip\" in provider reference), call API again. Wrap in a loop until text-only response.\n\n### Meta Moment\n\"I just used my Read tool to look at your code — that's the exact tool-use loop you just built. I sent a tool call, my runtime executed it, and the result came back to me. Your agent now works the same way.\"\n\n---\n\n## Step 6: Read File Tool\n\n### Concept\nOne tool is a proof of concept. Two tools require a dispatcher — routing logic that calls the right function based on the tool name. This is a pattern you'll use for every tool you add from here on.\n\n### Specification\nAdd the `read_file` tool:\n\n1. **Declare** a new tool in the request:\n - Name: `read_file`\n - Description: `\"Read the contents of a file at the given path\"`\n - Parameters: an object with a required `path` string property\n\n2. **Build a tool dispatcher**: instead of a single if-check, route by tool name:\n - `\"list_files\"` → list directory\n - `\"read_file\"` → read and return file contents\n\n3. **Implement `read_file`**: read the file at the given path and return its contents as a string. Handle errors (file not found) gracefully by returning an error message string rather than crashing.\n\n### Validation Criteria\n- [ ] `read_file` declared in tool definitions with correct schema\n- [ ] A dispatcher routes by function name (not a single hardcoded check)\n- [ ] `read_file` reads actual file contents from disk\n- [ ] Errors (e.g., file not found) return an error string, don't crash\n- [ ] Both `list_files` and `read_file` work correctly\n\n### Fast Track\nAdd `read_file` to tool definitions. Build a dispatcher that routes by tool name. Read and return file contents.\n\n### Meta Moment\n\"Notice how adding the second tool was mostly about the routing, not the tool itself. That's the pattern — the hard part was the agentic loop. From here, each new tool is just a new case in your dispatcher.\"\n\n---\n\n## Step 7: Bash Tool\n\n### Concept\nA bash/shell tool is what makes an agent truly powerful — it can run any command the user could run. This is also the most dangerous tool, so think about what guardrails make sense.\n\n### Specification\nAdd the `run_bash` tool:\n\n1. **Declare** in tool definitions:\n - Name: `run_bash`\n - Description: `\"Execute a bash command and return its output\"`\n - Parameters: an object with a required `command` string property\n\n2. **Implement**: spawn a subprocess that runs the command via the system shell. Capture both stdout and stderr. Return combined output as the tool result.\n\n3. **Timeout**: add a reasonable timeout (e.g., 30 seconds) to prevent hanging on long-running commands.\n\n4. **Error handling**: many commands exit with non-zero codes (e.g., `grep` finding no matches, `git diff` with changes). The agent must NOT crash on these — it should return the output (or error message) as a string, so the model can see what happened.\n\n **Important: wrap the entire command execution in a try/catch (or equivalent).** The key insight: a failed command still produces useful output. The tool should always return a string — either the stdout on success, or the stderr/error message on failure. Never let an exception bubble up and crash the agent.\n\n Guide the user toward this pattern:\n - Try to run the command\n - If it succeeds: return stdout (and stderr if any)\n - If it fails: catch the error, extract whatever output is available, return it as a string\n - Include the exit code in the returned string so the model knows what happened (e.g., `\"Exit code 1: No matches found\"`)\n\n### Validation Criteria\n- [ ] `run_bash` declared in tool definitions\n- [ ] Implementation spawns a subprocess / child process\n- [ ] Captures stdout and stderr\n- [ ] Returns output as tool result string\n- [ ] Has a timeout mechanism\n- [ ] Non-zero exit codes handled gracefully (returned, not crashed)\n\n### Fast Track\nAdd `run_bash` tool. Spawn a child process, capture stdout+stderr, return as result. Add a timeout. Handle non-zero exit codes.\n\n### Meta Moment\n\"Your agent can now run arbitrary commands — just like I can with my Bash tool. Try asking it to `git status` or `ls -la`. This is where it starts to feel like a real coding agent.\"\n\n---\n\n## Step 8: Edit File Tool (Optional)\n\n### Concept\nReading code is useful, but an agent that can modify code is transformative. This is the last tool that separates a \"read-only assistant\" from a true coding agent — with it, your agent can create and edit files just like the one you're talking to right now.\n\n**However:** by this point you've already learned everything about how agents work — the agentic loop, tool protocol, and dispatcher pattern are all in place. This step is purely about string manipulation and file I/O, not new agent concepts. If you want to wrap up here, your agent is already fully functional for reading, running commands, and answering questions about code.\n\n**Offer the user the choice:** implement the edit tool now, or skip to the completion celebration. If they skip, note that their agent can still create/edit files via `run_bash` — it's just less precise than a dedicated tool.\n\n### Specification\nAdd the `edit_file` tool:\n\n1. **Declare** in tool definitions:\n - Name: `edit_file`\n - Description: `\"Edit a file by replacing a specific string with new content. Can also create new files.\"`\n - Parameters: an object with three required string properties:\n - `path` — file path to edit or create\n - `old_string` — string to find and replace (empty string to create new file or append)\n - `new_string` — replacement string\n\n2. **Implement**:\n - If `old_string` is empty and file doesn't exist: create the file with `new_string` as contents\n - If `old_string` is empty and file exists: append `new_string` to the file\n - Otherwise: read the file, find `old_string`, replace with `new_string`, write back\n - If `old_string` is not found, return an error message\n - Validate that `old_string` appears exactly once to avoid ambiguous edits\n\n### Validation Criteria\n- [ ] `edit_file` declared in tool definitions with `path`, `old_string`, `new_string`\n- [ ] Handles file creation (empty `old_string`, file doesn't exist)\n- [ ] Handles find-and-replace in existing files\n- [ ] Returns error if `old_string` not found\n- [ ] Validates uniqueness of `old_string` (optional but good)\n- [ ] Actually writes changes to disk\n\n### Fast Track\nAdd `edit_file` with `path`, `old_string`, `new_string`. Empty `old_string` creates/appends. Otherwise find-and-replace. Error if not found.\n\n### Meta Moment\n\"You've now built a complete coding agent. List files, read files, run commands, edit files — that's the same core toolkit I'm working with right now. Everything else is refinement.\"\n\n### If the user skips\nMove directly to Completion. Their agent has 3 tools (list_files, read_file, run_bash) which is already a fully working coding agent — it can read code, run commands, and even create/edit files via bash. Congratulate them the same way.\n\n---\n\n## Completion\n\nWhen all steps are done (or the user skips Step 8), the user has a working coding agent with:\n- A conversational interface with memory\n- A system prompt that gives it identity and purpose\n- Three or four tools (list_files, read_file, run_bash, and optionally edit_file)\n- An agentic loop that keeps calling the API until the model stops requesting tools\n- ~250-300 lines of code, no SDKs, just raw HTTP\n\n### Celebrate and share\n\nCongratulate the user — they just built a coding agent from scratch. This is a real accomplishment worth sharing.\n\n**Offer to push to GitHub:**\n1. Ask if they'd like to push their agent to a GitHub repo on their personal account so they can share it with colleagues.\n2. If yes, help them set it up:\n - If git is available, the repo is already initialized with a commit per step — they can run `git log --oneline` to see their build history\n - Verify `.gitignore` is in place (`scaffold.sh` created it) — ensure `.build-agent-progress`, `.env`, `node_modules/`, etc. are listed\n - Create a short `README.md` with the agent's name, what it does, how to run it, and a note that it was built from scratch with no SDKs\n - Stage, commit the README, and push using `gh repo create \u003cagent-name> --public --source=. --push`\n3. Suggest they share the link with colleagues — it's a great conversation starter about how agents work under the hood. Anyone can clone it, set their API key, and try it out. If git tracking was active, the commit history shows the incremental build process step by step.\n\n**Frame it as a teaching moment:** \"You now understand how every coding agent works at its core — an LLM in a loop with tools. The next time you use any AI coding tool, you'll know exactly what's happening under the hood.\"\n\n### Suggested next steps for the user\n- Try asking the agent to search code — it can already do this via `run_bash` with `grep -rn` or `rg`\n- Implement streaming for faster-feeling responses\n- Add confirmation prompts before destructive operations (rm, overwriting files)\n- Token counting and context window management\n- Try pointing the agent at a different model/API (the patterns are the same)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":25243,"content_sha256":"beaac5b62ea94f40bf6a789ae8969dca214bec12b02a1efa831f5201f2be4b0a"},{"filename":"references/languages/go.md","content":"# Go Setup\n\n## Runtime\n\nRun with:\n```bash\ngo run .\n```\n\n## Project Setup Commands\n\nRun these Bash commands during Step 0 scaffold, in this order, before writing any files:\n\n```bash\nmkdir \u003cagent-name>\ncd \u003cagent-name> # or use the full path in subsequent commands\ngo mod init agent\n```\n\n`go mod init` creates the `go.mod` file that Go requires. Without it, `go run .` will fail with \"go.mod file not found\". This must be run as a Bash command — it cannot be replaced by a Write tool call.\n\n## Standard Library Packages\n\nAll stdlib, no `go get` needed:\n\n| Need | Import |\n|------|--------|\n| HTTP | `\"net/http\"` with `http.Post` or `http.NewRequest` |\n| JSON | `\"encoding/json\"` with `json.Marshal` / `json.Unmarshal` |\n| Stdin | `\"bufio\"`, `\"os\"` |\n| Run commands | `\"os/exec\"` with `exec.Command` |\n| Files | `\"os\"` (`os.ReadDir`, `os.ReadFile`, `os.WriteFile`) |\n\n## Notes\n\nGo users need to define structs for the request/response JSON. Suggest starting with a minimal struct and adding fields as needed. `map[string]any` works for quick prototyping but structs are better for tool definitions.\n\n## Language Hints for Specific Steps\n\n### Step 7 (Bash Tool): `exec.Command` returns an error on non-zero exit codes\nGo's `exec.Command(...).CombinedOutput()` returns both the output bytes AND an error when the command exits non-zero. The output is still populated — don't discard it. Use `CombinedOutput()` (captures stdout+stderr together), check the error, and return the output string either way. Use `context.WithTimeout` for the timeout.\n\n## Starter File\n\nWrite this as `main.go`. Replace `GEMINI_API_KEY` with the correct env var for the chosen provider (see Provider Env Vars in SKILL.md). For OpenAI, also add `baseURL` and `modelName` variables in `loadEnv()` with defaults from env or `\"https://api.openai.com/v1\"` / `\"gpt-4o\"`.\n\n```go\npackage main\n\nimport (\n\t\"bufio\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n)\n\nvar apiKey string\n\nfunc loadEnv() {\n\tdata, err := os.ReadFile(\".env\")\n\tif err != nil {\n\t\tfmt.Fprintln(os.Stderr, \"Could not read .env file:\", err)\n\t\tos.Exit(1)\n\t}\n\tfor _, line := range strings.Split(string(data), \"\\n\") {\n\t\tif key, val, ok := strings.Cut(line, \"=\"); ok {\n\t\t\tval = strings.TrimSpace(val)\n\t\t\tif val != \"\" && !strings.HasPrefix(val, \"#\") {\n\t\t\t\tos.Setenv(strings.TrimSpace(key), val)\n\t\t\t}\n\t\t}\n\t}\n\tapiKey = os.Getenv(\"GEMINI_API_KEY\")\n\tif apiKey == \"\" {\n\t\tfmt.Fprintln(os.Stderr, \"Missing GEMINI_API_KEY in .env file\")\n\t\tos.Exit(1)\n\t}\n}\n\nfunc main() {\n\tloadEnv()\n\tscanner := bufio.NewScanner(os.Stdin)\n\tfor {\n\t\tfmt.Print(\"> \")\n\t\tif !scanner.Scan() {\n\t\t\tbreak\n\t\t}\n\t\tinput := scanner.Text()\n\t\t// TODO: send to LLM API and print response\n\t\t_ = input\n\t}\n}\n```\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2675,"content_sha256":"a729b15bc01fb4eb3f18ea1d1fbae6bd571cad7a5c2edc41d8c43fe132cf6b3d"},{"filename":"references/languages/python.md","content":"# Python Setup\n\n## Runtime\n\n**Zero setup required.**\n\n```bash\nmkdir \u003cagent-name> && cd \u003cagent-name>\n```\n\nCreate `agent.py` and run with:\n```bash\npython3 agent.py\n```\n\n## Standard Library Modules\n\nAll stdlib, no pip install needed:\n\n| Need | Import |\n|------|--------|\n| HTTP | `from urllib.request import urlopen, Request` |\n| JSON | `import json` |\n| Stdin | `input()` built-in |\n| Run commands | `import subprocess` |\n| Files | `open()`, `import os`, `pathlib.Path` |\n\n## Starter File\n\nWrite this as `agent.py`. Replace `GEMINI_API_KEY` with the correct env var for the chosen provider (see Provider Env Vars in SKILL.md). For OpenAI, also add `BASE_URL` and `MODEL` variables after the API key check.\n\n```python\nimport json\nimport os\nfrom urllib.request import urlopen, Request\n\n# Load .env file\nwith open(\".env\") as f:\n for line in f:\n if \"=\" in line:\n key, value = line.strip().split(\"=\", 1)\n value = value.strip()\n if value and not value.startswith(\"#\"):\n os.environ[key.strip()] = value\n\nAPI_KEY = os.environ.get(\"GEMINI_API_KEY\")\nif not API_KEY:\n print(\"Missing GEMINI_API_KEY in .env file\")\n exit(1)\n\ndef main():\n while True:\n try:\n user_input = input(\"> \")\n except (EOFError, KeyboardInterrupt):\n break\n # TODO: send to LLM API and print response\n\nmain()\n```\n\n## Language Hints for Specific Steps\n\n### Step 7 (Bash Tool): `subprocess.run` doesn't throw on non-zero exit codes\nPython's `subprocess.run` returns a `CompletedProcess` object even when the command fails. Use `capture_output=True, text=True, timeout=30` and check `result.returncode`. Both `result.stdout` and `result.stderr` are always available. This is simpler than most languages — no try/catch needed for the exit code, just check the return code and combine stdout+stderr.\n\n## OpenAI Variant\n\nFor **OpenAI**, add after the API_KEY check:\n```python\nBASE_URL = os.environ.get(\"OPENAI_BASE_URL\", \"https://api.openai.com/v1\")\nMODEL = os.environ.get(\"MODEL_NAME\", \"gpt-4o\")\n```\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2063,"content_sha256":"753ce1db15769f76e2f989455e425b31ab42ba44c897fb6131200be6894ec1ab"},{"filename":"references/languages/ruby.md","content":"# Ruby Setup\n\n## Runtime\n\n**Zero setup required.**\n\n```bash\nmkdir \u003cagent-name> && cd \u003cagent-name>\n```\n\nCreate `agent.rb` and run with:\n```bash\nruby agent.rb\n```\n\n## Standard Library Modules\n\nAll stdlib, no gem install needed:\n\n| Need | Import |\n|------|--------|\n| HTTP | `require \"net/http\"` and `require \"uri\"` |\n| JSON | `require \"json\"` |\n| Stdin | `gets` or `$stdin.gets` |\n| Run commands | `require \"open3\"` with `Open3.capture3` |\n| Files | `File.read`, `File.write`, `Dir.entries` (built-in) |\n\n## Starter File\n\nWrite this as `agent.rb`. Replace `GEMINI_API_KEY` with the correct env var for the chosen provider (see Provider Env Vars in SKILL.md). For OpenAI, also add `BASE_URL` and `MODEL` variables after the API key check.\n\n```ruby\nrequire \"net/http\"\nrequire \"uri\"\nrequire \"json\"\n\n# Load .env file\nFile.readlines(\".env\").each do |line|\n key, value = line.strip.split(\"=\", 2)\n next unless key && value && !value.empty? && !value.start_with?(\"#\")\n ENV[key.strip] = value.strip\nend\n\nAPI_KEY = ENV[\"GEMINI_API_KEY\"]\nabort(\"Missing GEMINI_API_KEY in .env file\") unless API_KEY && !API_KEY.empty?\n\nloop do\n print \"> \"\n input = gets\n break if input.nil?\n input = input.chomp\n # TODO: send to LLM API and print response\nend\n```\n\n## Language Hints for Specific Steps\n\n### Step 7 (Bash Tool): `Open3.capture3` doesn't throw on non-zero exit codes\nRuby's `Open3.capture3` returns `[stdout, stderr, status]` even when the command fails. Check `status.exitstatus` for the exit code. Both stdout and stderr are always available. This is straightforward — no begin/rescue needed for the exit code.\n\n## OpenAI Variant\n\nFor **OpenAI**, add after the API_KEY check:\n```ruby\nBASE_URL = ENV[\"OPENAI_BASE_URL\"] || \"https://api.openai.com/v1\"\nMODEL = ENV[\"MODEL_NAME\"] || \"gpt-4o\"\n```\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":1786,"content_sha256":"2b1b0eec74d8fdd3555a1fd64b1b2ce0da96d21431b29b814141b63585d4b822"},{"filename":"references/languages/typescript.md","content":"# TypeScript Setup\n\n## Runtime\n\n**Recommended: `npx tsx`** (zero config, runs TypeScript directly).\n\n```bash\nmkdir \u003cagent-name> && cd \u003cagent-name>\n```\n\nCreate `agent.ts` and run with:\n```bash\nnpx tsx agent.ts\n```\n\nNo `package.json`, no `tsconfig.json`, no compilation step. `tsx` handles everything.\n\n**Alternative runtimes** (mention if the user prefers):\n- **Bun**: `bun run agent.ts` (also zero config)\n- **Node 23+**: `node --experimental-strip-types agent.ts`\n- **Node + tsc**: Requires `package.json` with `\"type\": \"module\"`, `tsconfig.json` — more setup, skip unless they specifically want it.\n\n## Standard Library Modules\n\nAll built-in, no npm install needed:\n\n| Need | Import |\n|------|--------|\n| HTTP | `fetch` (global, built-in) |\n| Stdin | `import * as readline from \"node:readline\"` |\n| Run commands | `import { execSync } from \"node:child_process\"` |\n| Files | `import { readFileSync, writeFileSync, readdirSync } from \"node:fs\"` |\n| JSON | Built-in `JSON.parse` / `JSON.stringify` |\n\n## Starter File\n\nWrite this as `agent.ts`. Replace `GEMINI_API_KEY` with the correct env var for the chosen provider (see Provider Env Vars in SKILL.md). For OpenAI, also add `BASE_URL` and `MODEL` variables after the API key check.\n\n```typescript\nimport * as readline from \"node:readline\";\nimport { readFileSync } from \"node:fs\";\n\n// Load .env file\nconst env = readFileSync(\".env\", \"utf-8\");\nfor (const line of env.split(\"\\n\")) {\n const [key, ...vals] = line.split(\"=\");\n if (key?.trim() && vals.length) {\n const v = vals.join(\"=\").trim();\n if (v && !v.startsWith(\"#\")) process.env[key.trim()] = v;\n }\n}\n\nconst API_KEY = process.env.GEMINI_API_KEY;\nif (!API_KEY) {\n console.error(\"Missing GEMINI_API_KEY in .env file\");\n process.exit(1);\n}\n\nconst rl = readline.createInterface({ input: process.stdin, output: process.stdout });\nconst prompt = (q: string): Promise\u003cstring> =>\n new Promise((resolve) => rl.question(q, resolve));\n\nasync function main() {\n while (true) {\n const input = await prompt(\"> \");\n // TODO: send to LLM API and print response\n }\n}\n\nmain().catch(console.error);\n```\n\n## Language Hints for Specific Steps\n\n### Step 7 (Bash Tool): `execSync` throws on non-zero exit codes\nUnlike most languages, Node's `execSync` throws an error when a command exits with a non-zero code. The error object has `stdout`, `stderr`, and `status` properties with the output. The user needs to wrap the call in try/catch and extract output from the error object on failure. This is the key gotcha — without the catch, commands like `grep` (exit 1 = no matches) will crash the agent.\n\n## OpenAI Variant\n\nFor **OpenAI**, add after the API_KEY check:\n```typescript\nconst BASE_URL = process.env.OPENAI_BASE_URL || \"https://api.openai.com/v1\";\nconst MODEL = process.env.MODEL_NAME || \"gpt-4o\";\n```\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":2815,"content_sha256":"7b11cb3b982984a56083d257460ef1172061f8e0be6e269bcdde194a201f4ae3"},{"filename":"references/providers/anthropic.md","content":"# Anthropic API Reference (Raw HTTP)\n\nQuick reference for making raw HTTP calls to the Anthropic Messages API. No SDK required — just HTTP POST with JSON.\n\n## Endpoint\n\n```\nPOST https://api.anthropic.com/v1/messages\n```\n\n**Default model:** `claude-sonnet-4-6`\n\n## Authentication\n\n| Header | Value |\n|--------|-------|\n| `x-api-key` | `YOUR_API_KEY` |\n| `anthropic-version` | `2023-06-01` |\n| `content-type` | `application/json` |\n\n**Environment variable:** `ANTHROPIC_API_KEY`\n\n**Note:** Anthropic uses `x-api-key` (not `Authorization: Bearer`). The `anthropic-version` header is required on every request.\n\n## `.env` file\n\n```\nANTHROPIC_API_KEY=your-api-key-here\n```\n\nGet a key at https://console.anthropic.com/settings/keys\n\n## Verify your key\n\n```bash\nsource .env && curl -s \"https://api.anthropic.com/v1/messages\" \\\n -H \"x-api-key: $ANTHROPIC_API_KEY\" \\\n -H \"anthropic-version: 2023-06-01\" \\\n -H \"content-type: application/json\" \\\n -d '{\"model\":\"claude-sonnet-4-6\",\"max_tokens\":50,\"messages\":[{\"role\":\"user\",\"content\":\"ping\"}]}' \\\n | head -c 200\n```\n\n---\n\n## Message Format\n\nMessages are sent as `messages[]`. Each message has:\n\n| Field | Type | Values |\n|-------|------|--------|\n| `role` | string | `\"user\"` or `\"assistant\"` |\n| `content` | string or array | Text string or array of content blocks |\n\n### Content block types\n\n**Text block:**\n```json\n{ \"type\": \"text\", \"text\": \"some text\" }\n```\n\n**Tool use block** (in assistant responses):\n```json\n{\n \"type\": \"tool_use\",\n \"id\": \"toolu_abc123\",\n \"name\": \"read_file\",\n \"input\": { \"path\": \"main.py\" }\n}\n```\n\n**Tool result block** (in user messages):\n```json\n{\n \"type\": \"tool_result\",\n \"tool_use_id\": \"toolu_abc123\",\n \"content\": \"file contents here\"\n}\n```\n\n**Note:** Anthropic only uses two roles: `user` and `assistant`. Tool results are sent as content blocks inside a `user`-role message.\n\n---\n\n## Request Body\n\n### Minimal (text only)\n\n```json\n{\n \"model\": \"claude-sonnet-4-6\",\n \"max_tokens\": 8096,\n \"messages\": [\n { \"role\": \"user\", \"content\": \"Hello!\" }\n ]\n}\n```\n\n**⚠️ `max_tokens` is required.** Unlike other APIs, Anthropic requires you to specify the maximum number of tokens in the response. Use `8096` as a sensible default.\n\n### Full (system prompt + tools)\n\n```json\n{\n \"model\": \"claude-sonnet-4-6\",\n \"max_tokens\": 8096,\n \"system\": \"You are a helpful coding assistant.\",\n \"messages\": [...],\n \"tools\": [{\n \"name\": \"list_files\",\n \"description\": \"List files and directories at the given path\",\n \"input_schema\": {\n \"type\": \"object\",\n \"properties\": {\n \"directory\": { \"type\": \"string\", \"description\": \"Directory path to list\" }\n },\n \"required\": [\"directory\"]\n }\n }]\n}\n```\n\n---\n\n## System Prompt\n\nThe system prompt is a top-level `system` field (sibling to `messages`):\n\n```json\n{\n \"model\": \"claude-sonnet-4-6\",\n \"max_tokens\": 8096,\n \"system\": \"You are a coding assistant named Marvin. Use tools proactively.\",\n \"messages\": [...]\n}\n```\n\nThe `system` field is a string (or an array of content blocks for advanced use). It is NOT part of the `messages` array.\n\n---\n\n## Response Format\n\n### Text response\n\n```json\n{\n \"id\": \"msg_abc123\",\n \"type\": \"message\",\n \"role\": \"assistant\",\n \"content\": [\n { \"type\": \"text\", \"text\": \"Response text here\" }\n ],\n \"stop_reason\": \"end_turn\"\n}\n```\n\n**Extract text:** `response.content[0].text` (when the block type is `\"text\"`)\n\n### Tool use response\n\n```json\n{\n \"id\": \"msg_abc123\",\n \"type\": \"message\",\n \"role\": \"assistant\",\n \"content\": [\n { \"type\": \"text\", \"text\": \"I'll check the directory for you.\" },\n {\n \"type\": \"tool_use\",\n \"id\": \"toolu_abc123\",\n \"name\": \"list_files\",\n \"input\": { \"directory\": \".\" }\n }\n ],\n \"stop_reason\": \"tool_use\"\n}\n```\n\n**Detection:** Check if any block in `response.content` has `type === \"tool_use\"`. You can also check `stop_reason === \"tool_use\"`.\n\n**Note:** Anthropic often includes both a text block AND a tool_use block in the same response. The text is the model \"thinking out loud\" before using the tool. Include the full content array in conversation history.\n\n### Multiple tool calls in one response\n\nThe model may return multiple `tool_use` blocks in a single response:\n\n```json\n{\n \"content\": [\n { \"type\": \"tool_use\", \"id\": \"toolu_abc\", \"name\": \"list_files\", \"input\": { \"directory\": \"src\" } },\n { \"type\": \"tool_use\", \"id\": \"toolu_def\", \"name\": \"read_file\", \"input\": { \"path\": \"README.md\" } }\n ]\n}\n```\n\nHandle each one and send all results back as separate `tool_result` blocks in a single `user`-role message.\n\n---\n\n## Tool Definitions\n\nTools are defined in the top-level `tools` array:\n\n```json\n{\n \"tools\": [{\n \"name\": \"list_files\",\n \"description\": \"List files and directories at the given path\",\n \"input_schema\": {\n \"type\": \"object\",\n \"properties\": {\n \"directory\": {\n \"type\": \"string\",\n \"description\": \"Directory path to list\"\n }\n },\n \"required\": [\"directory\"]\n }\n }]\n}\n```\n\n**Key difference from OpenAI/Gemini:** The schema field is called `input_schema`, not `parameters`. The schema format is standard JSON Schema.\n\n### Tool Choice (optional)\n\nControl how the model uses tools:\n\n```json\n{ \"tool_choice\": { \"type\": \"auto\" } }\n```\n\n| Value | Behavior |\n|-------|----------|\n| `{\"type\": \"auto\"}` | Model decides whether to call tools (default) |\n| `{\"type\": \"any\"}` | Model must call at least one tool |\n| `{\"type\": \"tool\", \"name\": \"...\"}` | Force a specific tool |\n\n---\n\n## Full Tool-Use Round Trip\n\n### 1. User sends a message with tool definitions\n\n```json\n{\n \"model\": \"claude-sonnet-4-6\",\n \"max_tokens\": 8096,\n \"messages\": [\n { \"role\": \"user\", \"content\": \"What files are in the current directory?\" }\n ],\n \"tools\": [{\n \"name\": \"list_files\",\n \"description\": \"List files and directories at the given path\",\n \"input_schema\": {\n \"type\": \"object\",\n \"properties\": {\n \"directory\": { \"type\": \"string\", \"description\": \"Directory path to list\" }\n },\n \"required\": [\"directory\"]\n }\n }]\n}\n```\n\n### 2. Model responds with a tool use\n\n```json\n{\n \"role\": \"assistant\",\n \"content\": [\n { \"type\": \"text\", \"text\": \"I'll list the files for you.\" },\n {\n \"type\": \"tool_use\",\n \"id\": \"toolu_abc123\",\n \"name\": \"list_files\",\n \"input\": { \"directory\": \".\" }\n }\n ],\n \"stop_reason\": \"tool_use\"\n}\n```\n\n### 3. Execute the tool and send the result back\n\nAppend the assistant's response AND the tool result to `messages`:\n\n```json\n{\n \"model\": \"claude-sonnet-4-6\",\n \"max_tokens\": 8096,\n \"messages\": [\n { \"role\": \"user\", \"content\": \"What files are in the current directory?\" },\n {\n \"role\": \"assistant\",\n \"content\": [\n { \"type\": \"text\", \"text\": \"I'll list the files for you.\" },\n { \"type\": \"tool_use\", \"id\": \"toolu_abc123\", \"name\": \"list_files\", \"input\": { \"directory\": \".\" } }\n ]\n },\n {\n \"role\": \"user\",\n \"content\": [{\n \"type\": \"tool_result\",\n \"tool_use_id\": \"toolu_abc123\",\n \"content\": \"main.py\\nREADME.md\\nutils.py\"\n }]\n }\n ],\n \"tools\": [{...same tool definitions...}]\n}\n```\n\n**Note:** Tool results are sent as `role: \"user\"` with `tool_result` content blocks — NOT a separate `\"tool\"` role.\n\n### 4. Model responds with text\n\n```json\n{\n \"role\": \"assistant\",\n \"content\": [\n { \"type\": \"text\", \"text\": \"The current directory contains three files: main.py, README.md, and utils.py.\" }\n ],\n \"stop_reason\": \"end_turn\"\n}\n```\n\n---\n\n## Common Pitfalls\n\n- **`max_tokens` is required**: Every request must include `max_tokens`. Omitting it returns an error. Use `8096` as a default.\n- **`anthropic-version` header is required**: Must be present on every request. Use `2023-06-01`.\n- **Tool results use `role: \"user\"`**: Unlike OpenAI (`role: \"tool\"`) and Gemini (`role: \"function\"`), Anthropic sends tool results as user messages with `tool_result` content blocks.\n- **`tool_use_id` must match**: Each `tool_result` block must reference the `id` from the corresponding `tool_use` block. Without it, the API returns an error.\n- **`input` is an object, not a string**: Unlike OpenAI where `arguments` is a JSON string, Anthropic's `input` is already a parsed object. No need to `JSON.parse()`.\n- **`input_schema` not `parameters`**: The tool schema field is called `input_schema` in Anthropic, not `parameters` as in OpenAI/Gemini.\n- **Content can be string or array**: For simple text, `content: \"hello\"` works. For tool results, `content` must be an array of content blocks. Be consistent — using arrays everywhere is safer.\n- **Include full assistant content**: When the model responds with both text and tool_use blocks, append the complete `content` array to conversation history — don't strip the text.\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":8759,"content_sha256":"7a8749bc7a6aac99db86be87dfd7b99a4d17ef75abbc8e8d8ed83888878d429e"},{"filename":"references/providers/gemini.md","content":"# Gemini API Reference (Raw HTTP)\n\nQuick reference for making raw HTTP calls to the Gemini API. No SDK required — just HTTP POST with JSON.\n\n## Endpoint\n\n```\nPOST https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=API_KEY\n```\n\n**Default model:** `gemini-2.5-flash`\n\n## Authentication\n\nTwo options (use whichever is easier in your language):\n\n| Method | Format |\n|--------|--------|\n| Query parameter | `?key=YOUR_API_KEY` |\n| Header | `x-goog-api-key: YOUR_API_KEY` |\n\n**Environment variable:** `GEMINI_API_KEY`\n\n## `.env` file\n\n```\nGEMINI_API_KEY=your-api-key-here\n```\n\nGet a free key at https://aistudio.google.com/apikey\n\n## Verify your key\n\n```bash\nsource .env && curl -s \"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=$GEMINI_API_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"contents\":[{\"role\":\"user\",\"parts\":[{\"text\":\"ping\"}]}]}' \\\n | head -c 200\n```\n\n---\n\n## Message Format\n\nMessages are sent as `contents[]`. Each message has:\n\n| Field | Type | Values |\n|-------|------|--------|\n| `role` | string | `\"user\"`, `\"model\"`, or `\"function\"` |\n| `parts` | array | Array of part objects |\n\n### Part types\n\n**Text:**\n```json\n{ \"text\": \"some text\" }\n```\n\n**Function call** (in model responses):\n```json\n{ \"functionCall\": { \"name\": \"read_file\", \"args\": { \"path\": \"main.py\" } } }\n```\n\n**Function response** (in function-role messages):\n```json\n{\n \"functionResponse\": {\n \"name\": \"read_file\",\n \"response\": { \"name\": \"read_file\", \"content\": \"file contents here\" }\n }\n}\n```\n\n---\n\n## Request Body\n\n### Minimal (text only)\n\n```json\n{\n \"contents\": [\n { \"role\": \"user\", \"parts\": [{ \"text\": \"Hello!\" }] }\n ],\n \"generationConfig\": { \"thinkingConfig\": { \"thinkingBudget\": 0 } }\n}\n```\n\n**⚠️ Always include `thinkingConfig: { thinkingBudget: 0 }`.** `gemini-2.5-flash` enables dynamic thinking by default. Without this, the model may return thinking parts (with `\"thought\": true`) before the actual text part, breaking the `parts[0].text` extraction path.\n\n### Full (system prompt + tools + thinking disabled)\n\n```json\n{\n \"systemInstruction\": {\n \"parts\": [{ \"text\": \"You are a helpful coding assistant.\" }]\n },\n \"contents\": [...],\n \"tools\": [{\n \"functionDeclarations\": [...]\n }],\n \"generationConfig\": {\n \"thinkingConfig\": { \"thinkingBudget\": 0 }\n }\n}\n```\n\n---\n\n## System Prompt\n\nUse `systemInstruction` at the top level of the request body (sibling to `contents`):\n\n```json\n{\n \"systemInstruction\": {\n \"parts\": [{ \"text\": \"You are a coding assistant named Marvin.\" }]\n },\n \"contents\": [...]\n}\n```\n\n**Important:** `systemInstruction` does NOT have a `role` field — only `parts`. It is NOT part of the `contents` array.\n\n---\n\n## Response Format\n\n### Text response\n\n```json\n{\n \"candidates\": [{\n \"content\": {\n \"parts\": [{ \"text\": \"Response text here\" }],\n \"role\": \"model\"\n },\n \"finishReason\": \"STOP\"\n }]\n}\n```\n\n**Extract text:** `response.candidates[0].content.parts[0].text`\n\n### Function call response\n\n```json\n{\n \"candidates\": [{\n \"content\": {\n \"parts\": [{\n \"functionCall\": {\n \"name\": \"list_files\",\n \"args\": { \"directory\": \".\" }\n }\n }],\n \"role\": \"model\"\n },\n \"finishReason\": \"STOP\"\n }]\n}\n```\n\n**Detection:** Check if any part in `candidates[0].content.parts` contains a `functionCall` object. Do NOT rely on `finishReason` — you must inspect the parts directly.\n\n### Multiple function calls in one response\n\nThe model may return multiple `functionCall` parts in a single response:\n\n```json\n{\n \"parts\": [\n { \"functionCall\": { \"name\": \"list_files\", \"args\": { \"directory\": \"src\" } } },\n { \"functionCall\": { \"name\": \"read_file\", \"args\": { \"path\": \"README.md\" } } }\n ]\n}\n```\n\nHandle each one and send all results back in a single `function`-role message with multiple `functionResponse` parts.\n\n---\n\n## Tool Definitions\n\nTools are defined in the `tools` array using `functionDeclarations`:\n\n```json\n{\n \"tools\": [{\n \"functionDeclarations\": [{\n \"name\": \"list_files\",\n \"description\": \"List files and directories at the given path\",\n \"parameters\": {\n \"type\": \"object\",\n \"properties\": {\n \"directory\": {\n \"type\": \"string\",\n \"description\": \"Directory path to list\"\n }\n },\n \"required\": [\"directory\"]\n }\n }]\n }]\n}\n```\n\nThe `parameters` field uses **OpenAPI 3.0 schema** format.\n\n### Tool Config (optional)\n\nControl how the model uses tools:\n\n```json\n{\n \"toolConfig\": {\n \"functionCallingConfig\": { \"mode\": \"AUTO\" }\n }\n}\n```\n\n| Mode | Behavior |\n|------|----------|\n| `AUTO` | Model decides whether to call tools (default) |\n| `ANY` | Model must call at least one tool |\n| `NONE` | Model cannot call tools |\n\n---\n\n## Full Tool-Use Round Trip\n\n### 1. User sends a message with tool definitions\n\n```json\n{\n \"contents\": [\n { \"role\": \"user\", \"parts\": [{ \"text\": \"What files are in the current directory?\" }] }\n ],\n \"tools\": [{\n \"functionDeclarations\": [{\n \"name\": \"list_files\",\n \"description\": \"List files and directories at the given path\",\n \"parameters\": {\n \"type\": \"object\",\n \"properties\": {\n \"directory\": { \"type\": \"string\", \"description\": \"Directory path to list\" }\n },\n \"required\": [\"directory\"]\n }\n }]\n }],\n \"generationConfig\": { \"thinkingConfig\": { \"thinkingBudget\": 0 } }\n}\n```\n\n### 2. Model responds with a function call\n\n```json\n{\n \"candidates\": [{\n \"content\": {\n \"parts\": [{ \"functionCall\": { \"name\": \"list_files\", \"args\": { \"directory\": \".\" } } }],\n \"role\": \"model\"\n }\n }]\n}\n```\n\n### 3. Execute the tool and send the result back\n\nAppend the model's response AND the tool result to `contents`:\n\n```json\n{\n \"contents\": [\n { \"role\": \"user\", \"parts\": [{ \"text\": \"What files are in the current directory?\" }] },\n {\n \"role\": \"model\",\n \"parts\": [{ \"functionCall\": { \"name\": \"list_files\", \"args\": { \"directory\": \".\" } } }]\n },\n {\n \"role\": \"function\",\n \"parts\": [{\n \"functionResponse\": {\n \"name\": \"list_files\",\n \"response\": { \"name\": \"list_files\", \"content\": \"main.py\\nREADME.md\\nutils.py\" }\n }\n }]\n }\n ],\n \"tools\": [{...same tool definitions...}],\n \"generationConfig\": { \"thinkingConfig\": { \"thinkingBudget\": 0 } }\n}\n```\n\n### 4. Model responds with text\n\n```json\n{\n \"candidates\": [{\n \"content\": {\n \"parts\": [{ \"text\": \"The current directory contains three files: main.py, README.md, and utils.py.\" }],\n \"role\": \"model\"\n }\n }]\n}\n```\n\n---\n\n## Common Pitfalls\n\n- **Forgetting to append the model's response**: Every API call must include the full conversation history in `contents`. After a model response, append its `content` as a `model`-role message.\n- **Wrong role for tool results**: Tool results use `role: \"function\"`, not `role: \"user\"`. Note: some newer Google documentation shows `role: \"user\"` for tool results — both work with the `v1beta` endpoint, but `role: \"function\"` is the canonical choice for `functionResponse` parts.\n- **Missing `response.name`**: The `functionResponse` object needs `name` at both the top level and inside `response`.\n- **Not checking parts for functionCall**: Tool calls are detected by inspecting `parts`, not `finishReason`.\n- **Sending empty contents**: The `contents` array must have at least one message.\n- **Thinking tokens**: With `thinkingBudget: 0`, thinking is disabled. If you enable thinking later, be aware that thinking parts appear in the response and should be filtered when displaying output.\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":7636,"content_sha256":"3e4e25fb6447f54857d6f3af0df9093521fcad4afe604678f92a9fe0073572b6"},{"filename":"references/providers/openai.md","content":"# OpenAI API Reference (Raw HTTP)\n\nQuick reference for making raw HTTP calls to the OpenAI Chat Completions API. No SDK required — just HTTP POST with JSON.\n\nThis reference also covers **OpenAI-compatible endpoints** (Ollama, Together AI, Groq, Azure OpenAI, LM Studio, vLLM, etc.) — same wire format, different base URL.\n\n## Endpoint\n\n```\nPOST https://api.openai.com/v1/chat/completions\n```\n\n**Default model:** `gpt-4o`\n\n### OpenAI-Compatible Endpoints\n\nAny service that implements the OpenAI Chat Completions API works with the same code. Just change the base URL and model name:\n\n| Provider | Base URL | Example model |\n|----------|----------|---------------|\n| OpenAI | `https://api.openai.com/v1` | `gpt-4o` |\n| Ollama (local) | `http://localhost:11434/v1` | `llama3.1` |\n| Together AI | `https://api.together.xyz/v1` | `meta-llama/Llama-3.1-70B-Instruct-Turbo` |\n| Groq | `https://api.groq.com/openai/v1` | `llama-3.3-70b-versatile` |\n| LM Studio (local) | `http://localhost:1234/v1` | `local-model` |\n| Azure OpenAI | `https://{resource}.openai.azure.com/openai/deployments/{deployment}` | deployment name |\n\n## Authentication\n\n| Header | Value |\n|--------|-------|\n| `Authorization` | `Bearer YOUR_API_KEY` |\n| `Content-Type` | `application/json` |\n\n**Environment variables:**\n- `OPENAI_API_KEY` — your API key\n- `OPENAI_BASE_URL` — (optional) override for compatible endpoints, defaults to `https://api.openai.com/v1`\n- `MODEL_NAME` — (optional) override model, defaults to `gpt-4o`\n\n## `.env` file\n\n```\nOPENAI_API_KEY=your-api-key-here\n# OPENAI_BASE_URL=https://api.openai.com/v1\n# MODEL_NAME=gpt-4o\n```\n\nGet a key at https://platform.openai.com/api-keys\n\nFor compatible endpoints, set the base URL and model for your provider.\n\n## Verify your key\n\n```bash\nsource .env && curl -s \"${OPENAI_BASE_URL:-https://api.openai.com/v1}/chat/completions\" \\\n -H \"Authorization: Bearer $OPENAI_API_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"model\":\"'\"${MODEL_NAME:-gpt-4o}\"'\",\"messages\":[{\"role\":\"user\",\"content\":\"ping\"}],\"max_tokens\":10}' \\\n | head -c 200\n```\n\n---\n\n## Message Format\n\nMessages are sent as `messages[]`. Each message has:\n\n| Field | Type | Values |\n|-------|------|--------|\n| `role` | string | `\"system\"`, `\"user\"`, `\"assistant\"`, or `\"tool\"` |\n| `content` | string or null | The message text (null for assistant messages with tool calls) |\n| `tool_calls` | array | (assistant only) Tool calls the model wants to make |\n| `tool_call_id` | string | (tool only) ID of the tool call this result is for |\n\n---\n\n## Request Body\n\n### Minimal (text only)\n\n```json\n{\n \"model\": \"gpt-4o\",\n \"messages\": [\n { \"role\": \"user\", \"content\": \"Hello!\" }\n ]\n}\n```\n\n### Full (system prompt + tools)\n\n```json\n{\n \"model\": \"gpt-4o\",\n \"messages\": [\n { \"role\": \"system\", \"content\": \"You are a helpful coding assistant.\" },\n { \"role\": \"user\", \"content\": \"What files are here?\" }\n ],\n \"tools\": [{\n \"type\": \"function\",\n \"function\": {\n \"name\": \"list_files\",\n \"description\": \"List files and directories at the given path\",\n \"parameters\": {\n \"type\": \"object\",\n \"properties\": {\n \"directory\": { \"type\": \"string\", \"description\": \"Directory path to list\" }\n },\n \"required\": [\"directory\"]\n }\n }\n }]\n}\n```\n\n---\n\n## System Prompt\n\nThe system prompt is just the first message with `role: \"system\"`:\n\n```json\n{\n \"model\": \"gpt-4o\",\n \"messages\": [\n { \"role\": \"system\", \"content\": \"You are a coding assistant named Marvin.\" },\n { \"role\": \"user\", \"content\": \"Hello!\" }\n ]\n}\n```\n\nThe system message is always the first element in the `messages` array and is sent with every request.\n\n---\n\n## Response Format\n\n### Text response\n\n```json\n{\n \"choices\": [{\n \"message\": {\n \"role\": \"assistant\",\n \"content\": \"Response text here\"\n },\n \"finish_reason\": \"stop\"\n }]\n}\n```\n\n**Extract text:** `response.choices[0].message.content`\n\n### Tool call response\n\n```json\n{\n \"choices\": [{\n \"message\": {\n \"role\": \"assistant\",\n \"content\": null,\n \"tool_calls\": [{\n \"id\": \"call_abc123\",\n \"type\": \"function\",\n \"function\": {\n \"name\": \"list_files\",\n \"arguments\": \"{\\\"directory\\\": \\\".\\\"}\"\n }\n }]\n },\n \"finish_reason\": \"tool_calls\"\n }]\n}\n```\n\n**Detection:** Check if `choices[0].message.tool_calls` exists and is non-empty. You can also check `finish_reason === \"tool_calls\"`.\n\n**⚠️ Important:** `function.arguments` is a **JSON string**, not an object. You must parse it: `JSON.parse(tool_call.function.arguments)`.\n\n### Multiple tool calls in one response\n\nThe model may return multiple tool calls in a single response:\n\n```json\n{\n \"tool_calls\": [\n { \"id\": \"call_abc\", \"type\": \"function\", \"function\": { \"name\": \"list_files\", \"arguments\": \"{\\\"directory\\\": \\\"src\\\"}\" } },\n { \"id\": \"call_def\", \"type\": \"function\", \"function\": { \"name\": \"read_file\", \"arguments\": \"{\\\"path\\\": \\\"README.md\\\"}\" } }\n ]\n}\n```\n\nHandle each one and send all results back as separate `tool`-role messages, each referencing its `tool_call_id`.\n\n---\n\n## Tool Definitions\n\nTools are defined in the `tools` array:\n\n```json\n{\n \"tools\": [{\n \"type\": \"function\",\n \"function\": {\n \"name\": \"list_files\",\n \"description\": \"List files and directories at the given path\",\n \"parameters\": {\n \"type\": \"object\",\n \"properties\": {\n \"directory\": {\n \"type\": \"string\",\n \"description\": \"Directory path to list\"\n }\n },\n \"required\": [\"directory\"]\n }\n }\n }]\n}\n```\n\nThe `parameters` field uses **JSON Schema** format (same as OpenAPI).\n\n### Tool Choice (optional)\n\nControl how the model uses tools:\n\n```json\n{ \"tool_choice\": \"auto\" }\n```\n\n| Value | Behavior |\n|-------|----------|\n| `\"auto\"` | Model decides whether to call tools (default) |\n| `\"required\"` | Model must call at least one tool |\n| `\"none\"` | Model cannot call tools |\n| `{\"type\": \"function\", \"function\": {\"name\": \"...\"}}` | Force a specific tool |\n\n---\n\n## Full Tool-Use Round Trip\n\n### 1. User sends a message with tool definitions\n\n```json\n{\n \"model\": \"gpt-4o\",\n \"messages\": [\n { \"role\": \"user\", \"content\": \"What files are in the current directory?\" }\n ],\n \"tools\": [{\n \"type\": \"function\",\n \"function\": {\n \"name\": \"list_files\",\n \"description\": \"List files and directories at the given path\",\n \"parameters\": {\n \"type\": \"object\",\n \"properties\": {\n \"directory\": { \"type\": \"string\", \"description\": \"Directory path to list\" }\n },\n \"required\": [\"directory\"]\n }\n }\n }]\n}\n```\n\n### 2. Model responds with a tool call\n\n```json\n{\n \"choices\": [{\n \"message\": {\n \"role\": \"assistant\",\n \"content\": null,\n \"tool_calls\": [{\n \"id\": \"call_abc123\",\n \"type\": \"function\",\n \"function\": {\n \"name\": \"list_files\",\n \"arguments\": \"{\\\"directory\\\": \\\".\\\"}\"\n }\n }]\n },\n \"finish_reason\": \"tool_calls\"\n }]\n}\n```\n\n### 3. Execute the tool and send the result back\n\nAppend the assistant's message (with tool_calls) AND the tool result to `messages`:\n\n```json\n{\n \"model\": \"gpt-4o\",\n \"messages\": [\n { \"role\": \"user\", \"content\": \"What files are in the current directory?\" },\n {\n \"role\": \"assistant\",\n \"content\": null,\n \"tool_calls\": [{\n \"id\": \"call_abc123\",\n \"type\": \"function\",\n \"function\": { \"name\": \"list_files\", \"arguments\": \"{\\\"directory\\\": \\\".\\\"}\" }\n }]\n },\n {\n \"role\": \"tool\",\n \"tool_call_id\": \"call_abc123\",\n \"content\": \"main.py\\nREADME.md\\nutils.py\"\n }\n ],\n \"tools\": [{...same tool definitions...}]\n}\n```\n\n### 4. Model responds with text\n\n```json\n{\n \"choices\": [{\n \"message\": {\n \"role\": \"assistant\",\n \"content\": \"The current directory contains three files: main.py, README.md, and utils.py.\"\n },\n \"finish_reason\": \"stop\"\n }]\n}\n```\n\n---\n\n## Common Pitfalls\n\n- **`arguments` is a JSON string**: You must `JSON.parse()` the `function.arguments` field. It's a string, not an object. This is the #1 gotcha.\n- **Forgetting `tool_call_id`**: Every tool result must reference the `id` from the corresponding `tool_calls` entry. Without it, the API returns an error.\n- **Forgetting to append the assistant message**: When the model makes tool calls, you must append its full message (including `tool_calls`) to the conversation before adding the tool results.\n- **`content: null` on tool call messages**: When the model makes tool calls, `content` is typically `null`. Don't crash on this — check for tool_calls first, then fall back to content.\n- **Model field is required**: Unlike Gemini (where the model is in the URL), OpenAI requires `model` in every request body.\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":8757,"content_sha256":"e3a3f998c9835bc023fcb057bd61a3e4d3cf42f72ac00fab2d3aacdddb454932"},{"filename":"scaffold.sh","content":"#!/usr/bin/env bash\nset -euo pipefail\n\n# scaffold.sh — Creates the build-agent tutorial project.\n#\n# Usage: scaffold.sh \u003cagent-name> \u003clanguage> \u003cprovider> \u003ctrack> [base-url] [model-name]\n#\n# Arguments:\n# agent-name Display name for the agent (e.g., Marvin)\n# language typescript | python | go | ruby\n# provider gemini | openai | anthropic\n# track guided | fast\n# base-url (optional) OpenAI-compatible base URL\n# model-name (optional) OpenAI-compatible model name\n\nif [[ $# -lt 4 ]]; then\n echo \"Usage: scaffold.sh \u003cagent-name> \u003clanguage> \u003cprovider> \u003ctrack> [base-url] [model-name]\" >&2\n exit 1\nfi\n\nAGENT_NAME=\"$1\"\nLANGUAGE=\"$2\"\nPROVIDER=\"$3\"\nTRACK=\"$4\"\nBASE_URL=\"${5:-}\"\nMODEL_NAME=\"${6:-}\"\n\n# Derive directory name (lowercase, spaces to hyphens)\nAGENT_DIR=\"$(echo \"$AGENT_NAME\" | tr '[:upper:]' '[:lower:]' | tr ' ' '-')\"\n\n# Validate language and set entry file / run command\ncase \"$LANGUAGE\" in\n typescript) ENTRY_FILE=\"agent.ts\"; RUN_CMD=\"npx tsx agent.ts\" ;;\n python) ENTRY_FILE=\"agent.py\"; RUN_CMD=\"python3 agent.py\" ;;\n go) ENTRY_FILE=\"main.go\"; RUN_CMD=\"go run .\" ;;\n ruby) ENTRY_FILE=\"agent.rb\"; RUN_CMD=\"ruby agent.rb\" ;;\n *) echo \"Unsupported language: $LANGUAGE\" >&2; exit 1 ;;\nesac\n\n# Validate provider and set env var / key URL\ncase \"$PROVIDER\" in\n gemini) ENV_VAR=\"GEMINI_API_KEY\"; KEY_URL=\"https://aistudio.google.com/apikey\" ;;\n openai) ENV_VAR=\"OPENAI_API_KEY\"; KEY_URL=\"https://platform.openai.com/api-keys\" ;;\n anthropic) ENV_VAR=\"ANTHROPIC_API_KEY\"; KEY_URL=\"https://console.anthropic.com/settings/keys\" ;;\n *) echo \"Unsupported provider: $PROVIDER\" >&2; exit 1 ;;\nesac\n\n# Validate track\ncase \"$TRACK\" in\n guided|fast) ;;\n *) echo \"Invalid track: $TRACK\" >&2; exit 1 ;;\nesac\n\nISO_DATE=\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"\n\n# --- Helpers ---\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\nTEMPLATE_DIR=\"$SCRIPT_DIR/templates\"\n\napply_template() {\n local template_file=\"$1\"; shift\n local content\n content=\"$(\u003c\"$template_file\")\"\n\n # Collect +BLOCK flags (sections to keep) and KEY VALUE pairs\n local -a keep_blocks=()\n local -a pairs=()\n while [[ $# -gt 0 ]]; do\n if [[ \"$1\" == +* ]]; then\n keep_blocks+=(\"${1#+}\")\n shift\n elif [[ $# -ge 2 ]]; then\n pairs+=(\"$1\" \"$2\"); shift 2\n else\n shift\n fi\n done\n\n # Process {{#BLOCK}}...{{/BLOCK}} conditional sections in one awk pass:\n # kept blocks have markers stripped; unmatched blocks are removed entirely\n if [[ \"$content\" == *'{{#'* ]]; then\n local keep_csv=\"\"\n for b in ${keep_blocks[@]+\"${keep_blocks[@]}\"}; do keep_csv=\"${keep_csv:+$keep_csv,}$b\"; done\n content=$(printf '%s\\n' \"$content\" | awk -v keeps=\"$keep_csv\" '\n BEGIN { n = split(keeps, a, \",\"); for (i = 1; i \u003c= n; i++) keep[a[i]] = 1 }\n /^\\{\\{#[A-Za-z_]+\\}\\}$/ {\n block = substr($0, 4, length($0) - 5)\n if (block in keep) next; skip = 1; next\n }\n /^\\{\\{\\/[A-Za-z_]+\\}\\}$/ {\n block = substr($0, 4, length($0) - 5)\n if (block in keep) next; skip = 0; next\n }\n !skip\n ')\n fi\n\n # Simple {{KEY}} → value substitution\n local i=0\n while [[ $i -lt ${#pairs[@]} ]]; do\n local key=\"${pairs[$i]}\" val=\"${pairs[$((i+1))]}\"\n content=\"${content//\\{\\{${key}\\}\\}/${val}}\"\n i=$((i + 2))\n done\n\n printf '%s\\n' \"$content\"\n}\n\n# --- Create project directory ---\n\nmkdir -p \"$AGENT_DIR\"\n\nif [[ -f \"$AGENT_DIR/$ENTRY_FILE\" ]]; then\n echo \"Note: $AGENT_DIR/ already exists, files will be overwritten\" >&2\nfi\n\n# --- Go: initialize module ---\n\nif [[ \"$LANGUAGE\" == \"go\" ]]; then\n if [[ ! -f \"$AGENT_DIR/go.mod\" ]]; then\n # Prefer `go mod init` for the correct version, fall back to template\n if (cd \"$AGENT_DIR\" && go mod init agent) 2>/dev/null; then\n : # success\n else\n cp \"$TEMPLATE_DIR/go/go.mod\" \"$AGENT_DIR/go.mod\"\n fi\n fi\nfi\n\n# --- Write starter code ---\n\nKEEP_BLOCKS=()\nif [[ \"$PROVIDER\" == \"openai\" ]]; then\n KEEP_BLOCKS=(+OPENAI)\nfi\napply_template \"$TEMPLATE_DIR/$LANGUAGE/$ENTRY_FILE\" \\\n API_KEY_VAR \"$ENV_VAR\" \\\n ${KEEP_BLOCKS[@]+\"${KEEP_BLOCKS[@]}\"} \\\n > \"$AGENT_DIR/$ENTRY_FILE\"\n\n# --- Write .env ---\n\ncase \"$PROVIDER\" in\n gemini)\n cat \u003c\u003c 'EOF' > \"$AGENT_DIR/.env\"\nGEMINI_API_KEY=your-api-key-here\nEOF\n ;;\n openai)\n if [[ -n \"$BASE_URL\" || -n \"$MODEL_NAME\" ]]; then\n cat > \"$AGENT_DIR/.env\" \u003c\u003c COMPAT_EOF\nOPENAI_API_KEY=your-api-key-here\nOPENAI_BASE_URL=${BASE_URL:-https://api.openai.com/v1}\nMODEL_NAME=${MODEL_NAME:-gpt-4o}\nCOMPAT_EOF\n else\n cat \u003c\u003c 'EOF' > \"$AGENT_DIR/.env\"\nOPENAI_API_KEY=your-api-key-here\n# OPENAI_BASE_URL=https://api.openai.com/v1\n# MODEL_NAME=gpt-4o\nEOF\n fi\n ;;\n anthropic)\n cat \u003c\u003c 'EOF' > \"$AGENT_DIR/.env\"\nANTHROPIC_API_KEY=your-api-key-here\nEOF\n ;;\nesac\n\n# --- Write .gitignore ---\n\ncp \"$TEMPLATE_DIR/$LANGUAGE/.gitignore\" \"$AGENT_DIR/.gitignore\"\n\n# --- Write AGENTS.md ---\n\napply_template \"$TEMPLATE_DIR/$LANGUAGE/AGENTS.md\" \\\n AGENT_NAME \"$AGENT_NAME\" PROVIDER \"$PROVIDER\" KEY_URL \"$KEY_URL\" \\\n > \"$AGENT_DIR/AGENTS.md\"\n\n# --- Write .build-agent-progress ---\n\ncat > \"$AGENT_DIR/.build-agent-progress\" \u003c\u003c PROGRESS_EOF\nagentName=$AGENT_NAME\nlanguage=$LANGUAGE\nprovider=$PROVIDER\nPROGRESS_EOF\n\nif [[ \"$PROVIDER\" == \"openai\" ]] && [[ -n \"$BASE_URL\" || -n \"$MODEL_NAME\" ]]; then\n cat >> \"$AGENT_DIR/.build-agent-progress\" \u003c\u003c COMPAT_EOF\nproviderBaseUrl=${BASE_URL:-https://api.openai.com/v1}\nproviderModel=${MODEL_NAME:-gpt-4o}\nCOMPAT_EOF\nfi\n\ncat >> \"$AGENT_DIR/.build-agent-progress\" \u003c\u003c PROGRESS_EOF\ntrack=$TRACK\ncurrentStep=1\ncompletedSteps=\nentryFile=$ENTRY_FILE\nlastUpdated=$ISO_DATE\nPROGRESS_EOF\n\n# --- Initialize git repo and commit initial scaffold ---\n\nif command -v git &>/dev/null; then\n if (\n cd \"$AGENT_DIR\"\n git init -q\n git config --get user.name >/dev/null 2>&1 || git config user.name \"Bloomery\"\n git config --get user.email >/dev/null 2>&1 || git config user.email \"bloomery@local\"\n git add -A\n git commit -q -m \"feat: scaffold $AGENT_NAME ($LANGUAGE/$PROVIDER)\"\n ) 2>/dev/null; then\n GIT_INIT=true\n else\n GIT_INIT=false\n fi\nelse\n GIT_INIT=false\nfi\n\n# --- Summary ---\n\necho \"\"\necho \"Created $AGENT_DIR/\"\necho \" $ENTRY_FILE ($LANGUAGE starter)\"\necho \" .env ($PROVIDER API key placeholder)\"\necho \" .gitignore\"\necho \" AGENTS.md\"\necho \" .build-agent-progress\"\nif [[ \"$LANGUAGE\" == \"go\" ]]; then\n echo \" go.mod\"\nfi\nif [[ \"$GIT_INIT\" == \"true\" ]]; then\n echo \" git repo initialized with initial commit\"\nfi\necho \"\"\necho \"Run: cd $AGENT_DIR && $RUN_CMD\"\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":6531,"content_sha256":"bacad948f5b899a6850307ae5b29528f315c9c103fd15d0cb1d02544ecdf54ec"},{"filename":"templates/go/AGENTS.md","content":"# {{AGENT_NAME}}\n\n{{PROVIDER}}/go coding agent built from scratch with raw HTTP calls.\n\n## Setup\n1. Add your API key to `.env`\n2. Key URL: {{KEY_URL}}\n\n## Run\n`go run .`\n\n## How it works\nAgentic loop: prompt -> LLM -> tool call -> execute -> result back -> LLM -> ... -> text response\n\n## Tools\n- [ ] list_files\n- [ ] read_file\n- [ ] run_bash\n- [ ] edit_file\n\n## Structure\n- `main.go` -- main agent source\n- `.env` -- API key (gitignored)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":439,"content_sha256":"3b96106d6128ff69431ac62f0827e651b43a21a24d0199074b1c10e60d160b64"},{"filename":"templates/go/main.go","content":"package main\n\nimport (\n\t\"bufio\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n)\n\nvar apiKey string\n{{#OPENAI}}\nvar baseURL string\nvar modelName string\n{{/OPENAI}}\n\nfunc loadEnv() {\n\tdata, err := os.ReadFile(\".env\")\n\tif err != nil {\n\t\tfmt.Fprintln(os.Stderr, \"Could not read .env file:\", err)\n\t\tos.Exit(1)\n\t}\n\tfor _, line := range strings.Split(string(data), \"\\n\") {\n\t\tif key, val, ok := strings.Cut(line, \"=\"); ok {\n\t\t\tval = strings.TrimSpace(val)\n\t\t\tif val != \"\" && !strings.HasPrefix(val, \"#\") {\n\t\t\t\tos.Setenv(strings.TrimSpace(key), val)\n\t\t\t}\n\t\t}\n\t}\n\tapiKey = os.Getenv(\"{{API_KEY_VAR}}\")\n\tif apiKey == \"\" {\n\t\tfmt.Fprintln(os.Stderr, \"Missing {{API_KEY_VAR}} in .env file\")\n\t\tos.Exit(1)\n\t}\n{{#OPENAI}}\n\tbaseURL = os.Getenv(\"OPENAI_BASE_URL\")\n\tif baseURL == \"\" {\n\t\tbaseURL = \"https://api.openai.com/v1\"\n\t}\n\tmodelName = os.Getenv(\"MODEL_NAME\")\n\tif modelName == \"\" {\n\t\tmodelName = \"gpt-4o\"\n\t}\n{{/OPENAI}}\n}\n\nfunc main() {\n\tloadEnv()\n\tscanner := bufio.NewScanner(os.Stdin)\n\tfor {\n\t\tfmt.Print(\"> \")\n\t\tif !scanner.Scan() {\n\t\t\tbreak\n\t\t}\n\t\tinput := scanner.Text()\n\t\t// TODO: send to LLM API and print response\n\t\t_ = input\n\t}\n}\n","content_type":"text/plain; charset=utf-8","language":"go","size":1100,"content_sha256":"0b4d8c5b82b0f980da513c75e97904cb4a3faa4fc8d3b73c0d6dd5376b3bc9cb"},{"filename":"templates/python/agent.py","content":"import json\nimport os\nimport sys\nfrom urllib.request import urlopen, Request\n\n# Load .env file\nwith open(\".env\") as f:\n for line in f:\n if \"=\" in line:\n key, value = line.strip().split(\"=\", 1)\n value = value.strip()\n if value and not value.startswith(\"#\"):\n os.environ[key.strip()] = value\n\nAPI_KEY = os.environ.get(\"{{API_KEY_VAR}}\")\nif not API_KEY:\n print(\"Missing {{API_KEY_VAR}} in .env file\", file=sys.stderr)\n exit(1)\n{{#OPENAI}}\n\nBASE_URL = os.environ.get(\"OPENAI_BASE_URL\", \"https://api.openai.com/v1\")\nMODEL = os.environ.get(\"MODEL_NAME\", \"gpt-4o\")\n{{/OPENAI}}\n\ndef main():\n while True:\n try:\n user_input = input(\"> \")\n except (EOFError, KeyboardInterrupt):\n break\n # TODO: send to LLM API and print response\n\nmain()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":836,"content_sha256":"67aa743dcf00b63d199e25091793185d668d157d6a06b0f502a8a3cb229d9672"},{"filename":"templates/python/AGENTS.md","content":"# {{AGENT_NAME}}\n\n{{PROVIDER}}/python coding agent built from scratch with raw HTTP calls.\n\n## Setup\n1. Add your API key to `.env`\n2. Key URL: {{KEY_URL}}\n\n## Run\n`python3 agent.py`\n\n## How it works\nAgentic loop: prompt -> LLM -> tool call -> execute -> result back -> LLM -> ... -> text response\n\n## Tools\n- [ ] list_files\n- [ ] read_file\n- [ ] run_bash\n- [ ] edit_file\n\n## Structure\n- `agent.py` -- main agent source\n- `.env` -- API key (gitignored)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":452,"content_sha256":"d0c53bbc6464efa4a4b77683e3fc7e09b763afea3f13ddac9bcc402be42b5ab5"},{"filename":"templates/ruby/AGENTS.md","content":"# {{AGENT_NAME}}\n\n{{PROVIDER}}/ruby coding agent built from scratch with raw HTTP calls.\n\n## Setup\n1. Add your API key to `.env`\n2. Key URL: {{KEY_URL}}\n\n## Run\n`ruby agent.rb`\n\n## How it works\nAgentic loop: prompt -> LLM -> tool call -> execute -> result back -> LLM -> ... -> text response\n\n## Tools\n- [ ] list_files\n- [ ] read_file\n- [ ] run_bash\n- [ ] edit_file\n\n## Structure\n- `agent.rb` -- main agent source\n- `.env` -- API key (gitignored)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":447,"content_sha256":"9ca287f89cab8d7ca97350834ebb3f0ce20b094f61cdbfb924125128aa47b122"},{"filename":"templates/typescript/agent.ts","content":"import * as readline from \"node:readline\";\nimport { readFileSync } from \"node:fs\";\n\n// Load .env file\nconst env = readFileSync(\".env\", \"utf-8\");\nfor (const line of env.split(\"\\n\")) {\n const [key, ...vals] = line.split(\"=\");\n if (key?.trim() && vals.length) {\n const v = vals.join(\"=\").trim();\n if (v && !v.startsWith(\"#\")) process.env[key.trim()] = v;\n }\n}\n\nconst API_KEY = process.env.{{API_KEY_VAR}};\nif (!API_KEY) {\n console.error(\"Missing {{API_KEY_VAR}} in .env file\");\n process.exit(1);\n}\n{{#OPENAI}}\n\nconst BASE_URL = process.env.OPENAI_BASE_URL || \"https://api.openai.com/v1\";\nconst MODEL = process.env.MODEL_NAME || \"gpt-4o\";\n{{/OPENAI}}\n\nconst rl = readline.createInterface({ input: process.stdin, output: process.stdout });\nconst prompt = (q: string): Promise\u003cstring> =>\n new Promise((resolve) => rl.question(q, resolve));\n\nasync function main() {\n while (true) {\n const input = await prompt(\"> \");\n // TODO: send to LLM API and print response\n }\n}\n\nmain().catch(console.error);\n","content_type":"text/typescript; charset=utf-8","language":"typescript","size":1010,"content_sha256":"9da07af5bca09c021d2f4e07168aef92fb936a94c87c1894527f74a192965bd8"},{"filename":"templates/typescript/AGENTS.md","content":"# {{AGENT_NAME}}\n\n{{PROVIDER}}/typescript coding agent built from scratch with raw HTTP calls.\n\n## Setup\n1. Add your API key to `.env`\n2. Key URL: {{KEY_URL}}\n\n## Run\n`npx tsx agent.ts`\n\n## How it works\nAgentic loop: prompt -> LLM -> tool call -> execute -> result back -> LLM -> ... -> text response\n\n## Tools\n- [ ] list_files\n- [ ] read_file\n- [ ] run_bash\n- [ ] edit_file\n\n## Structure\n- `agent.ts` -- main agent source\n- `.env` -- API key (gitignored)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":456,"content_sha256":"87d76667a31fd1a98abff057c0d72a6a554b9ead32351c7b87f8633213a3f64c"},{"filename":"tests/helpers/common.bash","content":"# helpers/common.bash — Shared setup/teardown and fixture helpers for BATS tests.\n\nSKILL_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")/../..\" && pwd)\"\n\nsetup() {\n TEST_TEMP_DIR=\"$(mktemp -d)\"\n cd \"$TEST_TEMP_DIR\" || return 1\n\n # Ensure git commits work even without global config (CI, containers)\n export GIT_AUTHOR_NAME=\"Test\"\n export GIT_AUTHOR_EMAIL=\"[email protected]\"\n export GIT_COMMITTER_NAME=\"Test\"\n export GIT_COMMITTER_EMAIL=\"[email protected]\"\n}\n\nteardown() {\n rm -rf \"$TEST_TEMP_DIR\"\n}\n\n# Wrappers merge stderr into stdout so BATS $output captures error messages too.\n\nrun_scaffold() {\n run bash -c '\"$@\" 2>&1' _ \"$SKILL_DIR/scaffold.sh\" \"$@\"\n}\n\nrun_progress_update() {\n run bash -c '\"$@\" 2>&1' _ \"$SKILL_DIR/progress-update.sh\" \"$@\"\n}\n\nrun_detect() {\n run bash -c '\"$@\" 2>&1' _ \"$SKILL_DIR/detect.sh\" \"$@\"\n}\n\n# create_progress_fixture \u003cdir>\n# Writes a minimal .build-agent-progress + AGENTS.md with known values.\n# Decouples progress-update tests from scaffold.sh correctness.\ncreate_progress_fixture() {\n local dir=\"${1:?directory required}\"\n\n mkdir -p \"$dir\"\n\n cat > \"$dir/.build-agent-progress\" \u003c\u003c 'EOF'\nagentName=TestAgent\nlanguage=typescript\nprovider=gemini\ntrack=guided\ncurrentStep=1\ncompletedSteps=\nentryFile=agent.ts\nlastUpdated=2024-01-01T00:00:00Z\nEOF\n\n cat > \"$dir/AGENTS.md\" \u003c\u003c 'EOF'\n# TestAgent\n\ngemini/typescript coding agent built from scratch with raw HTTP calls.\n\n## Tools\n- [ ] list_files\n- [ ] read_file\n- [ ] run_bash\n- [ ] edit_file\nEOF\n}\n","content_type":"text/plain; charset=utf-8","language":"bash","size":1478,"content_sha256":"c24488a8da116a833b0ccd15f8b1defaff5005586975c310936d195a597c555b"}],"content_json":{"type":"doc","content":[{"type":"heading","attrs":{"level":1},"content":[{"text":"Build-Agent Tutorial Skill","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Philosophy","type":"text"}]},{"type":"paragraph","content":[{"text":"You are a coding coach, not a code generator. By default, the user writes every line of code themselves. You guide, validate, and encourage. If they ask you to implement a step for them, confirm first — then do it.","type":"text"}]},{"type":"paragraph","content":[{"text":"Core rules:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"In ","type":"text"},{"text":"Step 0 ONLY","type":"text","marks":[{"type":"strong"}]},{"text":", scaffold the starter project by running the ","type":"text"},{"text":"scaffold.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":" script (directory, entry file with boilerplate stdin loop and imports, config files). This is the ONE exception — the boilerplate isn't the learning content, so we create it to get the user to the interesting part fast.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"After Step 0, do NOT use Write or Edit tools unless the user explicitly asks you to implement a step for them (escape hatch — see below).","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Do NOT output complete implementations unprompted. If you catch yourself writing more than a 5-line snippet, stop.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"ALWAYS read the user's code before giving feedback. Use the Read tool to see what they've actually written.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"ALWAYS validate the current step before advancing to the next one.","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"4-level hint escalation","type":"text","marks":[{"type":"strong"}]},{"text":" (use when the user is stuck):","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Conceptual nudge","type":"text","marks":[{"type":"strong"}]},{"text":": Restate the goal in different words. (\"Think about what data structure would let you keep all previous messages...\")","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Structural hint","type":"text","marks":[{"type":"strong"}]},{"text":": Name the specific construct or approach. (\"You'll need an array that lives outside the loop, and you append to it on each iteration.\")","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Pseudocode","type":"text","marks":[{"type":"strong"}]},{"text":": Show the logic without real syntax. (\"for each part in response.parts: if part has functionCall: execute(part.functionCall.name, part.functionCall.args)\")","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Small snippet","type":"text","marks":[{"type":"strong"}]},{"text":": As a last resort, show a minimal code fragment (3 lines max) for the specific thing they're stuck on. Never a full solution.","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Always start at level 1. Only escalate if the user is still stuck after trying.","type":"text"}]},{"type":"paragraph","content":[{"text":"Escape hatch — \"just do it for me\":","type":"text","marks":[{"type":"strong"}]},{"text":" If the user asks the agent to implement a step for them (e.g., \"just write it\", \"do it for me\", \"implement this step\"), don't refuse — but confirm first:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Say something like: \"Sure, I can implement this step for you. Just so you know, you'll learn the most by writing it yourself — but I understand if you want to move on. Want me to go ahead?\"","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"If they confirm: implement the step using Write/Edit tools, then validate the code as usual and advance.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"This is the ONE exception to the \"never write code after Step 0\" rule. The user is an adult — if they want to skip the hands-on part for a step, respect that. Some people learn by reading code too.","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Context Loading","type":"text"}]},{"type":"paragraph","content":[{"text":"This skill spans multiple reference files. Load them at the right time to keep context efficient.","type":"text"}]},{"type":"paragraph","content":[{"text":"On first invocation (Step 0):","type":"text","marks":[{"type":"strong"}]},{"text":" After the user answers the setup questions, use ","type":"text"},{"text":"Read","type":"text","marks":[{"type":"code_inline"}]},{"text":" to load exactly three files:","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"The provider reference for the chosen provider:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Gemini → ","type":"text"},{"text":"references/providers/gemini.md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"OpenAI (and compatible) → ","type":"text"},{"text":"references/providers/openai.md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Anthropic → ","type":"text"},{"text":"references/providers/anthropic.md","type":"text","marks":[{"type":"code_inline"}]}]}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"The language reference for the chosen language:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"TypeScript → ","type":"text"},{"text":"references/languages/typescript.md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Python → ","type":"text"},{"text":"references/languages/python.md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Go → ","type":"text"},{"text":"references/languages/go.md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Ruby → ","type":"text"},{"text":"references/languages/ruby.md","type":"text","marks":[{"type":"code_inline"}]}]}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"The curriculum: ","type":"text"},{"text":"references/curriculum.md","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"paragraph","content":[{"text":"Do NOT load more than one provider or language reference. For unsupported languages, skip the language reference and adapt from general knowledge.","type":"text"}]},{"type":"paragraph","content":[{"text":"On resume (progress file exists):","type":"text","marks":[{"type":"strong"}]}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Read ","type":"text"},{"text":".build-agent-progress","type":"text","marks":[{"type":"code_inline"}]},{"text":" to get the provider, language, and current step.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Use ","type":"text"},{"text":"Read","type":"text","marks":[{"type":"code_inline"}]},{"text":" to load the matching provider reference, language reference, and curriculum.","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"During the tutorial:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"These files are already loaded — do not re-read them each step.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"When giving hints or answering API format questions, consult the loaded references rather than writing out full JSON yourself.","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Step 0 — Greet and Orient","type":"text"}]},{"type":"paragraph","content":[{"text":"When first invoked, do the following:","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Explain what they'll build, then present all setup questions in a single message — STOP and wait for the user's reply.","type":"text","marks":[{"type":"strong"}]},{"text":" Do NOT load context files, scaffold the project, or do anything else until the user has responded. Keep the explanation brief, then present the four questions below. The user can answer in one reply (e.g., \"1, 3, 1, Marvin\").","type":"text"}]},{"type":"paragraph","content":[{"text":"Brief explanation: A working coding agent in ~300 lines — no frameworks, no SDKs, just raw HTTP calls to an LLM API. They're using a coding agent to learn how to build one.","type":"text"}]},{"type":"paragraph","content":[{"text":"Then present exactly these four questions:","type":"text"}]},{"type":"paragraph","content":[{"text":"Which LLM provider?","type":"text","marks":[{"type":"strong"}]}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Google Gemini (free tier, recommended)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"OpenAI / OpenAI-compatible (Ollama, Together AI, Groq, etc.)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Anthropic (Claude)","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Which language?","type":"text","marks":[{"type":"strong"}]}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"TypeScript (recommended)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Python","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Go","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Ruby","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Other","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Which track?","type":"text","marks":[{"type":"strong"}]}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Guided — concept explanations, detailed specs with JSON examples, and meta moments connecting what you build to how this agent works (~60-90 min)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Fast Track — one-line specs pointing to the provider reference, same validation, minimal hand-holding (~30-45 min)","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"What should we name your agent?","type":"text","marks":[{"type":"strong"}]},{"text":" (e.g., Jarvis, Friday, Marvin, Devin't, Cody — or pick your own)","type":"text"}]},{"type":"paragraph","content":[{"text":"If they chose OpenAI-compatible, also ask for base URL and model name (defaults: ","type":"text"},{"text":"https://api.openai.com/v1","type":"text","marks":[{"type":"code_inline"}]},{"text":" and ","type":"text"},{"text":"gpt-4o","type":"text","marks":[{"type":"code_inline"}]},{"text":").","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Parsing the user's reply","type":"text"}]},{"type":"paragraph","content":[{"text":"The user will typically reply with three numbers and a name, e.g., \"1, 3, 1, Marvin\" or \"1 3 1 Marvin\". Parse the values ","type":"text"},{"text":"positionally","type":"text","marks":[{"type":"strong"}]},{"text":" using these lookup tables:","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Position","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Question","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"1","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"2","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"3","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"4","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"5","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"1st","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Provider","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"gemini","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"openai","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"anthropic","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"—","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"—","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"2nd","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Language","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"typescript","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"python","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"go","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"ruby","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"(other)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"3rd","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Track","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"guided","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"fast","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"—","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"—","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"—","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"4th","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Name","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"(free text — the agent's name)","type":"text","marks":[{"type":"em"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph"}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph"}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph"}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph"}]}]}]},{"type":"paragraph","content":[{"text":"Example","type":"text","marks":[{"type":"strong"}]},{"text":": \"1, 3, 1, Marvin\" → provider=gemini, language=go, track=guided, name=Marvin ","type":"text"},{"text":"Example","type":"text","marks":[{"type":"strong"}]},{"text":": \"2, 1, 2, Friday\" → provider=openai, language=typescript, track=fast, name=Friday","type":"text"}]},{"type":"paragraph","content":[{"text":"CRITICAL","type":"text","marks":[{"type":"strong"}]},{"text":": Do NOT assume defaults or skip the lookup. Map each number through the table above. Getting the language wrong wastes the user's time by scaffolding the wrong project.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Load context files","type":"text","marks":[{"type":"strong"}]},{"text":": Only after the user has replied, follow the Context Loading instructions — ","type":"text"},{"text":"Read","type":"text","marks":[{"type":"code_inline"}]},{"text":" the provider reference, language reference, and curriculum.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Set up the project","type":"text","marks":[{"type":"strong"}]},{"text":" — run the scaffold script to create everything in one command.","type":"text"}]},{"type":"paragraph","content":[{"text":"a.","type":"text","marks":[{"type":"strong"}]},{"text":" ","type":"text"},{"text":"Bash","type":"text","marks":[{"type":"code_inline"}]},{"text":": Run ","type":"text"},{"text":"scaffold.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":" from this skill's directory. Use the same base path where you loaded this SKILL.md from:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"bash \u003cskill-dir>/scaffold.sh \"\u003cagent-name>\" \u003clanguage> \u003cprovider> \u003ctrack>","type":"text"}]},{"type":"paragraph","content":[{"text":"Example: ","type":"text"},{"text":"bash /path/to/skills/bloomery/scaffold.sh \"Marvin\" typescript gemini guided","type":"text","marks":[{"type":"code_inline"}]}]},{"type":"paragraph","content":[{"text":"For OpenAI-compatible endpoints, append the base URL and model name:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"bash \u003cskill-dir>/scaffold.sh \"\u003cagent-name>\" \u003clanguage> openai \u003ctrack> \"\u003cbase-url>\" \"\u003cmodel-name>\"","type":"text"}]},{"type":"paragraph","content":[{"text":"The script creates: the project directory (lowercased agent name), starter file with boilerplate stdin loop, ","type":"text"},{"text":".env","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":".gitignore","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"AGENTS.md","type":"text","marks":[{"type":"code_inline"}]},{"text":", and ","type":"text"},{"text":".build-agent-progress","type":"text","marks":[{"type":"code_inline"}]},{"text":". For Go, it also runs ","type":"text"},{"text":"go mod init","type":"text","marks":[{"type":"code_inline"}]},{"text":". When ","type":"text"},{"text":"git","type":"text","marks":[{"type":"code_inline"}]},{"text":" is available, it also initializes a local git repository and creates an initial commit (","type":"text"},{"text":"feat: scaffold \u003cname> (\u003clanguage>/\u003cprovider>)","type":"text","marks":[{"type":"code_inline"}]},{"text":"), so the user can have version history from the start.","type":"text"}]},{"type":"paragraph","content":[{"text":"b.","type":"text","marks":[{"type":"strong"}]},{"text":" Tell the user how to run their agent and verify it works (should prompt for input, print \"TODO\" or similar for the LLM call).","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Verify API key","type":"text","marks":[{"type":"strong"}]},{"text":": Tell the user to open the ","type":"text"},{"text":".env","type":"text","marks":[{"type":"code_inline"}]},{"text":" file and replace the placeholder with their actual API key. Point them to the right URL to get a key:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Gemini","type":"text","marks":[{"type":"strong"}]},{"text":": https://aistudio.google.com/apikey (free tier)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"OpenAI","type":"text","marks":[{"type":"strong"}]},{"text":": https://platform.openai.com/api-keys","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Anthropic","type":"text","marks":[{"type":"strong"}]},{"text":": https://console.anthropic.com/settings/keys","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Important: Warn the user NOT to paste their API key into this chat window.","type":"text","marks":[{"type":"strong"}]},{"text":" Anything typed here goes to an LLM API and could end up in conversation logs. The ","type":"text"},{"text":".env","type":"text","marks":[{"type":"code_inline"}]},{"text":" file is the safe place for it — and it's already in ","type":"text"},{"text":".gitignore","type":"text","marks":[{"type":"code_inline"}]},{"text":" so it won't be committed.","type":"text"}]},{"type":"paragraph","content":[{"text":"Once they've saved the ","type":"text"},{"text":".env","type":"text","marks":[{"type":"code_inline"}]},{"text":" file, run the provider-specific curl test from the provider reference to verify it works.","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Then proceed to Step 1.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Provider Env Vars","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Provider","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Env vars","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Key URL","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Gemini","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"GEMINI_API_KEY","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"https://aistudio.google.com/apikey","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"OpenAI","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"OPENAI_API_KEY","type":"text","marks":[{"type":"code_inline"}]},{"text":", optionally ","type":"text"},{"text":"OPENAI_BASE_URL","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"MODEL_NAME","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"https://platform.openai.com/api-keys","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Anthropic","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"ANTHROPIC_API_KEY","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"https://console.anthropic.com/settings/keys","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":".env","type":"text","marks":[{"type":"code_inline"}]},{"text":" templates","type":"text"}]},{"type":"blockquote","content":[{"type":"paragraph","content":[{"text":"These templates are for reference — ","type":"text"},{"text":"scaffold.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":" writes the correct ","type":"text"},{"text":".env","type":"text","marks":[{"type":"code_inline"}]},{"text":" automatically.","type":"text"}]}]},{"type":"paragraph","content":[{"text":"Gemini:","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"GEMINI_API_KEY=your-api-key-here","type":"text"}]},{"type":"paragraph","content":[{"text":"OpenAI:","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"OPENAI_API_KEY=your-api-key-here\n# OPENAI_BASE_URL=https://api.openai.com/v1\n# MODEL_NAME=gpt-4o","type":"text"}]},{"type":"paragraph","content":[{"text":"OpenAI-compatible (e.g., Ollama, Together AI, Groq):","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"OPENAI_API_KEY=your-api-key-here\nOPENAI_BASE_URL=https://your-provider-url/v1\nMODEL_NAME=your-model-name","type":"text"}]},{"type":"paragraph","content":[{"text":"Anthropic:","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"ANTHROPIC_API_KEY=your-api-key-here","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Project Setup by Language","type":"text"}]},{"type":"paragraph","content":[{"text":"The ","type":"text"},{"text":"scaffold.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":" script handles all project setup — directory creation, starter file, ","type":"text"},{"text":".env","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":".gitignore","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"AGENTS.md","type":"text","marks":[{"type":"code_inline"}]},{"text":", progress file, and Go module init. The agent just runs it with the user's choices.","type":"text"}]},{"type":"paragraph","content":[{"text":"The language reference files (","type":"text"},{"text":"references/languages/*.md","type":"text","marks":[{"type":"code_inline"}]},{"text":") are still loaded during Context Loading — they contain stdlib module tables and step-specific language hints needed during the tutorial.","type":"text"}]},{"type":"paragraph","content":[{"text":"For unsupported languages, skip the scaffold script and help the user set up based on general knowledge. The requirements are simple: HTTP POST with JSON, stdin loop, subprocess execution, file I/O.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Progress File","type":"text"}]},{"type":"paragraph","content":[{"text":"The skill persists tutorial state in a ","type":"text"},{"text":".build-agent-progress","type":"text","marks":[{"type":"code_inline"}]},{"text":" file (key=value format) in the project directory. This lets users stop and resume the tutorial across sessions.","type":"text"}]},{"type":"paragraph","content":[{"text":"Format (initial state after Step 0):","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"agentName=Marvin\nlanguage=typescript\nprovider=gemini\ntrack=guided\ncurrentStep=1\ncompletedSteps=\nentryFile=agent.ts\nlastUpdated=2025-06-15T10:30:00Z","type":"text"}]},{"type":"paragraph","content":[{"text":"After each validated step, increment ","type":"text"},{"text":"currentStep","type":"text","marks":[{"type":"code_inline"}]},{"text":" and append the completed step number to the comma-separated ","type":"text"},{"text":"completedSteps","type":"text","marks":[{"type":"code_inline"}]},{"text":" list. Example after completing Steps 1 and 2: ","type":"text"},{"text":"currentStep=3","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"completedSteps=1,2","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]},{"type":"paragraph","content":[{"text":"For OpenAI-compatible endpoints, two extra lines appear after ","type":"text"},{"text":"provider","type":"text","marks":[{"type":"code_inline"}]},{"text":":","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"provider=openai\nproviderBaseUrl=https://api.together.xyz/v1\nproviderModel=meta-llama/Llama-3.1-70B-Instruct-Turbo","type":"text"}]},{"type":"paragraph","content":[{"text":"When to write:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Step 0","type":"text","marks":[{"type":"strong"}]},{"text":": Created automatically by ","type":"text"},{"text":"scaffold.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":" — no manual step needed.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"After each step is validated","type":"text","marks":[{"type":"strong"}]},{"text":": Update ","type":"text"},{"text":"currentStep","type":"text","marks":[{"type":"code_inline"}]},{"text":" to the next step and append the completed step to ","type":"text"},{"text":"completedSteps","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"When to read:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"On every invocation","type":"text","marks":[{"type":"strong"}]},{"text":": Before doing anything else, check for ","type":"text"},{"text":".build-agent-progress","type":"text","marks":[{"type":"code_inline"}]},{"text":" in the current directory. If it exists, read it and use the stored state. Greet the user by their agent's name and offer to continue from where they left off. Load the appropriate provider reference.","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Progress Detection","type":"text"}]},{"type":"paragraph","content":[{"text":"On every invocation, detect where the user is using a two-layer approach:","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Layer 1: Progress file (fast, reliable)","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Run ","type":"text"},{"text":"detect.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":" from this skill's directory:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"bash \u003cskill-dir>/detect.sh \u003cproject-directory>","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"If the output has ","type":"text"},{"text":"\"found\": true","type":"text","marks":[{"type":"code_inline"}]},{"text":" and ","type":"text"},{"text":"\"source\": \"progress_file\"","type":"text","marks":[{"type":"code_inline"}]},{"text":", you have the agent name, language, provider, track, and current step.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Load the provider reference from ","type":"text"},{"text":"references/providers/{provider}.md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Greet the user: \"Welcome back! Last time we were working on [agent name] — you're on Step N ([step title]). Ready to continue?\"","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Layer 2: Code scanning (fallback, verification)","type":"text"}]},{"type":"paragraph","content":[{"text":"If no progress file exists, ","type":"text"},{"text":"detect.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":" automatically falls back to code scanning. The output will have ","type":"text"},{"text":"\"source\": \"code_scan\"","type":"text","marks":[{"type":"code_inline"}]},{"text":" with the detected language, provider, entry file, and step.","type":"text"}]},{"type":"paragraph","content":[{"text":"If the detected step differs from the progress file, trust the code — the user may have kept coding after the session ended. Update the progress file to match.","type":"text"}]},{"type":"paragraph","content":[{"text":"If no progress file exists but code is found (the output has ","type":"text"},{"text":"\"found\": true, \"source\": \"code_scan\"","type":"text","marks":[{"type":"code_inline"}]},{"text":"), create the progress file based on what ","type":"text"},{"text":"detect.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":" found (ask for agent name and track if not known).","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Reporting","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Report what you found: \"Welcome back! I can see [agent name] has completed through Step N. Ready to continue with Step N+1?\"","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"If the user's code has issues in a completed step, flag them: \"Before we move on, I noticed [issue] in your Step N implementation. Want to fix that first?\"","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Step Dispatch","type":"text"}]},{"type":"paragraph","content":[{"text":"For each step, follow the curriculum in ","type":"text"},{"text":"references/curriculum.md","type":"text","marks":[{"type":"code_inline"}]},{"text":":","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Guided Track flow:","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Introduce the concept","type":"text","marks":[{"type":"strong"}]},{"text":" (2-3 sentences from the Concept section)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Give the specification","type":"text","marks":[{"type":"strong"}]},{"text":" — follow the curriculum's specification for the step. When it says \"show the user the exact format from the provider reference,\" actually pull the specific JSON example from the loaded provider reference and include it in your explanation. Don't just say \"see the reference\" — show them the concrete structure they need. Keep it focused: only the specific section they need for this step, not the whole reference.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Let them code","type":"text","marks":[{"type":"strong"}]},{"text":" — stop talking, let them write. They'll tell you when they're ready for review or if they're stuck.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Validate (MANDATORY GATE)","type":"text","marks":[{"type":"strong"}]},{"text":" — use ","type":"text"},{"text":"Read","type":"text","marks":[{"type":"code_inline"}]},{"text":" to read their code. Check against EVERY item in the validation criteria from the curriculum. Give specific, actionable feedback. See \"Validation Gate\" below.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Surface the meta moment","type":"text","marks":[{"type":"strong"}]},{"text":" — briefly point out the connection between what they just built and how the coding agent they're using right now works.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Immediately advance","type":"text","marks":[{"type":"strong"}]},{"text":" — don't wait for the user to say \"next\" or \"continue.\" Once validation passes: run ","type":"text"},{"text":"progress-update.sh \u003cagent-dir> \u003ccompleted-step>","type":"text","marks":[{"type":"code_inline"}]},{"text":" (if the project is a git repo, this also commits the step's changes with a conventional commit like ","type":"text"},{"text":"feat(step-N): \u003ctitle>","type":"text","marks":[{"type":"code_inline"}]},{"text":"), then in the SAME message, start the next step (introduce its concept + give its specification). Keep the momentum going.","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Fast Track flow:","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Give the fast track spec","type":"text","marks":[{"type":"strong"}]},{"text":" from the curriculum (one-line summary + pointer to provider reference)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Let them code","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Validate (MANDATORY GATE)","type":"text","marks":[{"type":"strong"}]},{"text":" — same criteria, more concise feedback. See \"Validation Gate\" below.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Immediately advance","type":"text","marks":[{"type":"strong"}]},{"text":" — once validation passes, run ","type":"text"},{"text":"progress-update.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":" (if the project is a git repo, this also commits to git), then start the next step in the same message.","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Validation Gate","type":"text"}]},{"type":"paragraph","content":[{"text":"This is the most important rule in the entire skill.","type":"text","marks":[{"type":"strong"}]},{"text":" You MUST NOT advance to the next step unless the current step's code is actually implemented and passes validation. No exceptions — ","type":"text"},{"text":"except Step 8 (Edit File Tool), which is explicitly optional.","type":"text","marks":[{"type":"strong"}]},{"text":" The user may skip it and go straight to Completion.","type":"text"}]},{"type":"paragraph","content":[{"text":"On every step transition:","type":"text","marks":[{"type":"strong"}]}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Use ","type":"text"},{"text":"Read","type":"text","marks":[{"type":"code_inline"}]},{"text":" to read the user's source file. Always. Even if they say \"I did it\" or \"let's move on\".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Check each validation criterion from the curriculum. Be specific — don't just skim, actually verify each checkbox. Where criteria say \"see provider reference\", check the user's code against the loaded provider reference for format correctness.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"If any criterion is not met:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Tell the user exactly what's missing or broken, with line references.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Do NOT proceed to the next step. Stay on the current step.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"If the user asks to skip or move on anyway, explain that each step builds on the last and skipping will cause problems later. Offer to help them through whatever they're stuck on.","type":"text"}]}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"If all criteria are met: congratulate briefly, run ","type":"text"},{"text":"progress-update.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":" to update progress, then ","type":"text"},{"text":"immediately introduce the next step in the same message","type":"text","marks":[{"type":"strong"}]},{"text":" — don't stop and wait for the user to say \"next.\"","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"If the user says \"move on\" or \"skip this\" but the code isn't ready:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Read their code first. Always verify.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Say something like: \"Let me check your code first — [agent name] needs [missing thing] before we can move on, otherwise Step N+1 won't work because it builds on this. Here's what's missing: [specifics]. Want me to help you through it?\"","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Be firm but supportive. The whole point of the tutorial is that they write working code at each step.","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Meta Moments","type":"text"}]},{"type":"paragraph","content":[{"text":"At key steps, briefly point out that the coding agent the user is talking to right now is doing exactly what they're implementing. Keep these short and genuine — one or two sentences max. Don't be cheesy.","type":"text"}]},{"type":"paragraph","content":[{"text":"Examples:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"After Step 1: \"You just built a chat interface — the same thing you're talking to me through right now. The difference is I have memory, identity, and tools.\"","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"After Step 4: \"I just used my Read tool to check your code — that's the exact tool-use loop you just built.\"","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"After Step 7: \"Your agent can now run shell commands, just like I can with my Bash tool.\"","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"After Step 8: \"Your agent has the same core toolkit I'm working with right now.\"","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Language-Specific Guidance","type":"text"}]},{"type":"paragraph","content":[{"text":"The language reference file (loaded during Context Loading) contains stdlib module tables and language-specific notes. Consult it when helping with implementation details. For unsupported languages, adapt from general knowledge — the concepts are universal: HTTP POST, JSON parsing, stdin loop, subprocess execution, file I/O.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Troubleshooting","type":"text"}]},{"type":"paragraph","content":[{"text":"Common problems and how to handle them:","type":"text"}]},{"type":"paragraph","content":[{"text":"API key verification fails (curl returns error):","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"401/403: Key is wrong or not activated yet. Have the user double-check the key in ","type":"text"},{"text":".env","type":"text","marks":[{"type":"code_inline"}]},{"text":". For Gemini, ensure it's a Generative Language API key (not a Cloud API key).","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Connection error: Check internet connectivity. For OpenAI-compatible endpoints, verify the base URL is correct.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"400: Request format is wrong. Check the curl command matches the provider reference exactly.","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"User's code doesn't compile or crashes on run:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Read their code with the ","type":"text"},{"text":"Read","type":"text","marks":[{"type":"code_inline"}]},{"text":" tool. Look for syntax errors, missing imports, or typos.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"If it's a runtime error (e.g., JSON parse failure), the API likely returned an error response. Suggest they print the raw response body before parsing to see what came back.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Common: forgetting to set ","type":"text"},{"text":"Content-Type: application/json","type":"text","marks":[{"type":"code_inline"}]},{"text":" header, forgetting ","type":"text"},{"text":"max_tokens","type":"text","marks":[{"type":"code_inline"}]},{"text":" for Anthropic, malformed JSON body.","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"API returns 400/error during a step:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"The model may be returning an error they aren't handling. Suggest they print the full response status and body.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"For Anthropic: forgetting ","type":"text"},{"text":"anthropic-version","type":"text","marks":[{"type":"code_inline"}]},{"text":" header or ","type":"text"},{"text":"max_tokens","type":"text","marks":[{"type":"code_inline"}]},{"text":" field.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"For OpenAI: forgetting the ","type":"text"},{"text":"model","type":"text","marks":[{"type":"code_inline"}]},{"text":" field in the request body.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"For tool steps: malformed ","type":"text"},{"text":"functionResponse","type":"text","marks":[{"type":"code_inline"}]},{"text":"/","type":"text"},{"text":"tool_result","type":"text","marks":[{"type":"code_inline"}]},{"text":" structure is the most common cause.","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"User is stuck and escalation isn't helping:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"After level 3 (pseudocode), offer to look at their code together. Read it, identify the specific gap, and give a targeted 3-line snippet.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"If they're fundamentally confused about the API format, suggest they re-read the provider reference section that covers their current step.","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Rules","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Scaffold the project in Step 0.","type":"text","marks":[{"type":"strong"}]},{"text":" Run the ","type":"text"},{"text":"scaffold.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":" script from this skill's directory to create the starter file with boilerplate (stdin loop, imports, TODO comments), ","type":"text"},{"text":".env","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":".gitignore","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"AGENTS.md","type":"text","marks":[{"type":"code_inline"}]},{"text":", and ","type":"text"},{"text":".build-agent-progress","type":"text","marks":[{"type":"code_inline"}]},{"text":". Do not create these files manually — the script handles all provider/language variations. This gets the user past the boring setup and into the real learning.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"After Step 0, don't write code for the user — unless they explicitly ask.","type":"text","marks":[{"type":"strong"}]},{"text":" From Step 1 onward, do not use Write or Edit tools ","type":"text"},{"text":"except","type":"text","marks":[{"type":"strong"}]},{"text":" to run ","type":"text"},{"text":"progress-update.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":" after each validated step, ","type":"text"},{"text":"or","type":"text","marks":[{"type":"strong"}]},{"text":" when the user triggers the escape hatch (asks you to implement a step for them — confirm first, then do it). The user writes all the agent logic themselves by default. In your text responses, keep code snippets to 5 lines max and only as a last resort when they're stuck.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"NEVER advance without validating.","type":"text","marks":[{"type":"strong"}]},{"text":" This is non-negotiable. Before moving to the next step, ALWAYS use ","type":"text"},{"text":"Read","type":"text","marks":[{"type":"code_inline"}]},{"text":" to check their actual code against every validation criterion. Don't take their word for it — look at the code. If the user says \"skip\" or \"move on\" but the code isn't there, refuse politely and explain what's missing. Each step builds on the last; skipping creates compounding problems.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Keep explanations concise.","type":"text","marks":[{"type":"strong"}]},{"text":" The user is here to code, not to read essays. Two to three sentences for a concept, then let them work.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Be encouraging but honest.","type":"text","marks":[{"type":"strong"}]},{"text":" Celebrate progress. But if there's a bug, say so clearly and help them find it. Don't gloss over issues.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Use the provider reference as your source of truth.","type":"text","marks":[{"type":"strong"}]},{"text":" When showing the user JSON formats, pull the specific snippet they need from the loaded provider reference — don't invent examples from memory. Show only the section relevant to the current step, not the whole reference.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Track state across invocations.","type":"text","marks":[{"type":"strong"}]},{"text":" Use progress detection to pick up where the user left off. Don't make them repeat themselves.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Adapt to the user's pace.","type":"text","marks":[{"type":"strong"}]},{"text":" If they're breezing through, skip the hand-holding. If they're struggling, slow down and use the hint escalation. Meet them where they are.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Stay provider-aware.","type":"text","marks":[{"type":"strong"}]},{"text":" Always use the correct terminology for the user's provider (e.g., ","type":"text"},{"text":"functionCall","type":"text","marks":[{"type":"code_inline"}]},{"text":" for Gemini, ","type":"text"},{"text":"tool_calls","type":"text","marks":[{"type":"code_inline"}]},{"text":" for OpenAI, ","type":"text"},{"text":"tool_use","type":"text","marks":[{"type":"code_inline"}]},{"text":" for Anthropic). Don't mix provider terms — it causes confusion.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Stay language-aware.","type":"text","marks":[{"type":"strong"}]},{"text":" When showing pseudocode, examples, or snippets, always use the syntax of the user's chosen language. Don't show Python-style pseudocode to a TypeScript user or vice versa. If the curriculum contains language-neutral descriptions, adapt them to the user's language when presenting.","type":"text"}]}]}]},{"type":"hr","attrs":{"markup":"---"}}]},"metadata":{"date":"2026-06-05","name":"bloomery","author":"@skillopedia","source":{"stars":29,"repo_name":"bloomery","origin_url":"https://github.com/mgratzer/bloomery/blob/HEAD/skills/bloomery/SKILL.md","repo_owner":"mgratzer","body_sha256":"cc4b67df3ec6f98546c31a85edf04606b189474c0384354f6a52a3f650d3489a","cluster_key":"256f2db0082194c6da7aea611e2d3497249e592f53947c5d87510a736f74da33","clean_bundle":{"format":"clean-skill-bundle-v1","source":"mgratzer/bloomery/skills/bloomery/SKILL.md","attachments":[{"id":"fd8c699f-7e9d-5d9c-8ba7-66edd47bc365","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/fd8c699f-7e9d-5d9c-8ba7-66edd47bc365/attachment.sh","path":"detect.sh","size":3550,"sha256":"d9698af23b94f80c4633b75d185e2516f83cd4747f27e2a4781016827072149b","contentType":"application/x-sh; charset=utf-8"},{"id":"e26026fb-784b-590a-b0cd-b154d69a5560","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/e26026fb-784b-590a-b0cd-b154d69a5560/attachment.sh","path":"progress-update.sh","size":3083,"sha256":"e08e5151765676c23de899b1df33a2aca78facc56d1fc0e2da5426d53addbf64","contentType":"application/x-sh; charset=utf-8"},{"id":"85bb53d0-c5dc-57c1-9477-fce302d36a34","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/85bb53d0-c5dc-57c1-9477-fce302d36a34/attachment.md","path":"references/curriculum.md","size":25243,"sha256":"beaac5b62ea94f40bf6a789ae8969dca214bec12b02a1efa831f5201f2be4b0a","contentType":"text/markdown; charset=utf-8"},{"id":"1c13ff8c-b027-51f5-b780-6a08ccc958f5","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/1c13ff8c-b027-51f5-b780-6a08ccc958f5/attachment.md","path":"references/languages/go.md","size":2675,"sha256":"a729b15bc01fb4eb3f18ea1d1fbae6bd571cad7a5c2edc41d8c43fe132cf6b3d","contentType":"text/markdown; charset=utf-8"},{"id":"5eccf2d4-ab68-55c4-885d-3be55b4410b6","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/5eccf2d4-ab68-55c4-885d-3be55b4410b6/attachment.md","path":"references/languages/python.md","size":2063,"sha256":"753ce1db15769f76e2f989455e425b31ab42ba44c897fb6131200be6894ec1ab","contentType":"text/markdown; charset=utf-8"},{"id":"fe59bef1-90d9-5b5e-ab7d-7d458ecd5a29","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/fe59bef1-90d9-5b5e-ab7d-7d458ecd5a29/attachment.md","path":"references/languages/ruby.md","size":1786,"sha256":"2b1b0eec74d8fdd3555a1fd64b1b2ce0da96d21431b29b814141b63585d4b822","contentType":"text/markdown; charset=utf-8"},{"id":"9c49164f-4a75-580c-beec-6aac0a756614","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/9c49164f-4a75-580c-beec-6aac0a756614/attachment.md","path":"references/languages/typescript.md","size":2815,"sha256":"7b11cb3b982984a56083d257460ef1172061f8e0be6e269bcdde194a201f4ae3","contentType":"text/markdown; charset=utf-8"},{"id":"707ed760-c1c8-5037-b0ae-b088f21eef46","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/707ed760-c1c8-5037-b0ae-b088f21eef46/attachment.md","path":"references/providers/anthropic.md","size":8759,"sha256":"7a8749bc7a6aac99db86be87dfd7b99a4d17ef75abbc8e8d8ed83888878d429e","contentType":"text/markdown; charset=utf-8"},{"id":"a4e0f44b-fdfa-526c-a455-58d5dab84d19","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/a4e0f44b-fdfa-526c-a455-58d5dab84d19/attachment.md","path":"references/providers/gemini.md","size":7636,"sha256":"3e4e25fb6447f54857d6f3af0df9093521fcad4afe604678f92a9fe0073572b6","contentType":"text/markdown; charset=utf-8"},{"id":"c0eeb7f5-721d-504b-b542-7517529621c7","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/c0eeb7f5-721d-504b-b542-7517529621c7/attachment.md","path":"references/providers/openai.md","size":8757,"sha256":"e3a3f998c9835bc023fcb057bd61a3e4d3cf42f72ac00fab2d3aacdddb454932","contentType":"text/markdown; charset=utf-8"},{"id":"60fb6553-fdb7-53b4-a684-dd725e9ba232","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/60fb6553-fdb7-53b4-a684-dd725e9ba232/attachment.sh","path":"scaffold.sh","size":6531,"sha256":"bacad948f5b899a6850307ae5b29528f315c9c103fd15d0cb1d02544ecdf54ec","contentType":"application/x-sh; charset=utf-8"},{"id":"9dc34b1a-39bf-5f32-9519-bc2b5f95a5e3","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/9dc34b1a-39bf-5f32-9519-bc2b5f95a5e3/attachment","path":"templates/go/.gitignore","size":36,"sha256":"9b39ad9461782a3c421f1bd738843eb1fe07cb6ec62191ed91f4c746d2212b88","contentType":"text/plain; charset=utf-8"},{"id":"81dff409-a502-5df7-96fa-4a0c7e34afac","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/81dff409-a502-5df7-96fa-4a0c7e34afac/attachment.md","path":"templates/go/AGENTS.md","size":439,"sha256":"3b96106d6128ff69431ac62f0827e651b43a21a24d0199074b1c10e60d160b64","contentType":"text/markdown; charset=utf-8"},{"id":"9abbfb9e-efeb-51bf-a5ac-d3217e6a70ef","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/9abbfb9e-efeb-51bf-a5ac-d3217e6a70ef/attachment.mod","path":"templates/go/go.mod","size":22,"sha256":"9f79907b485ef07cb1fef97dece646852edda0cf62ecdeb25561944de7725b91","contentType":"application/xml-dtd"},{"id":"5a886c2f-e214-525e-b1b0-cb1de25e738a","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/5a886c2f-e214-525e-b1b0-cb1de25e738a/attachment.go","path":"templates/go/main.go","size":1100,"sha256":"0b4d8c5b82b0f980da513c75e97904cb4a3faa4fc8d3b73c0d6dd5376b3bc9cb","contentType":"text/plain; charset=utf-8"},{"id":"407c1d00-8289-56c6-8a4e-8553c490bfa8","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/407c1d00-8289-56c6-8a4e-8553c490bfa8/attachment","path":"templates/python/.gitignore","size":36,"sha256":"9b39ad9461782a3c421f1bd738843eb1fe07cb6ec62191ed91f4c746d2212b88","contentType":"text/plain; charset=utf-8"},{"id":"8a3a53c5-90dc-5d7d-8d0f-c426606771fe","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/8a3a53c5-90dc-5d7d-8d0f-c426606771fe/attachment.md","path":"templates/python/AGENTS.md","size":452,"sha256":"d0c53bbc6464efa4a4b77683e3fc7e09b763afea3f13ddac9bcc402be42b5ab5","contentType":"text/markdown; charset=utf-8"},{"id":"4101d6f1-2d72-5021-a164-ef9b93af24a7","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/4101d6f1-2d72-5021-a164-ef9b93af24a7/attachment.py","path":"templates/python/agent.py","size":836,"sha256":"67aa743dcf00b63d199e25091793185d668d157d6a06b0f502a8a3cb229d9672","contentType":"text/x-python; charset=utf-8"},{"id":"28f62e76-e9c7-54da-9544-e45633cefeee","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/28f62e76-e9c7-54da-9544-e45633cefeee/attachment","path":"templates/ruby/.gitignore","size":36,"sha256":"9b39ad9461782a3c421f1bd738843eb1fe07cb6ec62191ed91f4c746d2212b88","contentType":"text/plain; charset=utf-8"},{"id":"a6302822-cd68-55c6-96f7-b4fb6bc4b1fc","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/a6302822-cd68-55c6-96f7-b4fb6bc4b1fc/attachment.md","path":"templates/ruby/AGENTS.md","size":447,"sha256":"9ca287f89cab8d7ca97350834ebb3f0ce20b094f61cdbfb924125128aa47b122","contentType":"text/markdown; charset=utf-8"},{"id":"db0ea09b-4d98-5e57-be06-8c870ca703c0","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/db0ea09b-4d98-5e57-be06-8c870ca703c0/attachment.rb","path":"templates/ruby/agent.rb","size":622,"sha256":"bdb2ac38f98130560f6002f8caecdc870f3d10a595a0d7db7151c213ec590eec","contentType":"application/x-ruby"},{"id":"14ca5203-1bb3-534a-af0a-b36ec3c66108","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/14ca5203-1bb3-534a-af0a-b36ec3c66108/attachment","path":"templates/typescript/.gitignore","size":50,"sha256":"706de7af2f4f73ef4eb976dcca76102392f9a9393c6cfd5efb90339241a077df","contentType":"text/plain; charset=utf-8"},{"id":"bbc38aa5-ce3d-51fe-8392-28d0c6aae900","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/bbc38aa5-ce3d-51fe-8392-28d0c6aae900/attachment.md","path":"templates/typescript/AGENTS.md","size":456,"sha256":"87d76667a31fd1a98abff057c0d72a6a554b9ead32351c7b87f8633213a3f64c","contentType":"text/markdown; charset=utf-8"},{"id":"3e72bddb-b936-512a-8f81-28c438155e78","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/3e72bddb-b936-512a-8f81-28c438155e78/attachment.ts","path":"templates/typescript/agent.ts","size":1010,"sha256":"9da07af5bca09c021d2f4e07168aef92fb936a94c87c1894527f74a192965bd8","contentType":"text/typescript; charset=utf-8"},{"id":"f0d7a4c0-72a6-505b-a863-ad317e22558b","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/f0d7a4c0-72a6-505b-a863-ad317e22558b/attachment.bats","path":"tests/detect.bats","size":13674,"sha256":"06134fa999f55a7cf19eef642733e8360631291a7f193e866956f075f3efc204","contentType":"text/plain; charset=utf-8"},{"id":"757e54d6-06fa-528a-a01f-95cdbba50701","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/757e54d6-06fa-528a-a01f-95cdbba50701/attachment.bash","path":"tests/helpers/common.bash","size":1478,"sha256":"c24488a8da116a833b0ccd15f8b1defaff5005586975c310936d195a597c555b","contentType":"text/plain; charset=utf-8"},{"id":"058548e1-2667-5c6f-8914-6f812d7ad87f","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/058548e1-2667-5c6f-8914-6f812d7ad87f/attachment.bats","path":"tests/progress-update.bats","size":6149,"sha256":"d99b2a0493fdb9a01f905863564eb2cc5be7c3a232db80d9d050c06988db7724","contentType":"text/plain; charset=utf-8"},{"id":"4e211043-8369-59ad-996f-aa293798b67c","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/4e211043-8369-59ad-996f-aa293798b67c/attachment.bats","path":"tests/scaffold.bats","size":9564,"sha256":"e3be3af658932a549b68dff99002933d857378de704da502b84de9d2be46d585","contentType":"text/plain; charset=utf-8"}],"bundle_sha256":"ab5cb4432919c8fa45745bcb5d2e81443e6e6fc65b18d09d831f0082cbf30acb","attachment_count":28,"text_attachments":20,"attachment_storage":"skillopedia-attachments-v1","binary_attachments":8,"excluded_attachments":[]},"cluster_size":1,"skill_md_path":"skills/bloomery/SKILL.md","import_metadata":{"date":"2026-06-05","author":"@skillopedia","version":"v1","category":"web-development","category_label":"Web"},"exact_dupes_collapsed_into_this":0},"version":"v1","category":"web-development","import_tag":"clean-skills-v1","description":"Interactive tutorial that guides engineers through building their own coding agent (agentic loop) from scratch using raw HTTP calls to an LLM API. Supports Gemini, OpenAI (and compatible endpoints), and Anthropic. Supports TypeScript, Python, Go, and Ruby. Detects progress automatically. Use when someone says \"build an agent\", \"teach me agents\", or \"/build-agent\".\n","allowed-tools":"Read, Write, Edit, Bash, Grep, Glob"}},"renderedAt":1782980243874}

Build-Agent Tutorial Skill Philosophy You are a coding coach, not a code generator. By default, the user writes every line of code themselves. You guide, validate, and encourage. If they ask you to implement a step for them, confirm first — then do it. Core rules: - In Step 0 ONLY , scaffold the starter project by running the script (directory, entry file with boilerplate stdin loop and imports, config files). This is the ONE exception — the boilerplate isn't the learning content, so we create it to get the user to the interesting part fast. - After Step 0, do NOT use Write or Edit tools unle…