装配车间里,零件在长桌上摊了一地,都是这些日子一件件磨出来的。
先立起来的是那间玻璃房——里头坐着个口若悬河的天才,可四壁不透气,他一根手指也伸不出来。于是墙上开一扇递物窗:他每提一个要求,窗外的人就去办,办完把东西塞回窗内,一来一回,直到他说"齐了"才算一程。窗外又砌起一面工具墙,每个取用口都卡着一副卡尺——不合规格的手根本伸不进去。每件工具还配一把三档锁:绿档直接用,红档焊死(比如"把整间屋子推平"这种),黄档得先按铃等人点头。桌沿那摞纸越堆越高,快顶到桌角那块从不许占的留白时,就有人把最早几叠誊成一页摘要、原件收进档案柜,腾出手来接着干。碰上"得翻半屋子档案才答得上"的活,他掐诀放出一个影分身:分身领一张干净空桌出门,草稿全留在门外,回来只交一页结论。
零件齐了,还差两样,今天一并装上。一样是一块铭牌,刻着这台机器是谁、在谁的院子里干活、动手前先看后做——它挂在门楣上,只对每一次开口念一遍,却从不混进那摞纸里,所以誊摘要时绝不会被誊掉。另一样是门后一只计数器:拨到头它就自己停,免得这机器无人看管时空转到天荒地老。
老师傅拿起最后一颗螺丝,对准、拧紧。玻璃房里的天才,如今能看、能查、能动手、动手前有人把关、说错话有人拦、干太久会自己收——他不再只是个隔着玻璃的顾问了。
把一个只会说话的模型,一件件裹进循环、工具、闸门、摘要、分身、身份与预算这层外壳,让它真能在世界里动手办事——这层外壳,就叫 harness;而今天,是它的组装日。
先猜揭晓前,先押一个猜测 · 你觉得这讲的是?
谜底 · The Concept
Stage 6 — assembly: system prompt + loop + tools + gate + compaction + sub-agents = a mini Claude Code
Build From Scratch · 从零构建
中文速览 · Quick read
Stage 6 introduces no new organ. It is the assembly. The five stages before it each grew one organ in isolation — the loop (loop.ts), real tools (tools.ts), the permission gate (permission.ts), compaction (compact.ts), and sub-agents (agent.ts's spawn_agent). The goal here is to wire all five into one running process, and to bolt on the last two organs the earlier stages never needed standalone: an explicit identity (a system prompt) and an explicit budget (a turn cap). The result is a mini Claude Code — small enough to read in one sitting, but with one of every organ a real agent harness has.
The whole point of the tutorial lives in this file: none of these organs is impressive alone. A loop with no gate is a chainsaw with no guard; a gate with no loop never gets asked twice. What makes an agent is the composition — and the composition is what breaks, organ by organ, when you pull a piece out. The demo above is exactly that experiment: flip one organ off, run "fix the failing test," and watch which step of the run falls over.
The keystone move is in tutorial/06-harness/loop.ts: the while-body from stage 4 is pulled out into a reusable function so it can run twice in one process — once for the top-level agent, once (with a different tool set and a fresh, empty messages) for each sub-agent:
export async function runAgentLoop(client: Anthropic, opts: RunAgentLoopOptions): Promise<string> {
const { model, messages, tools, execute, label, maxTurns = 20, system } = opts;
let lastText = '';
for (let turn = 0; turn < maxTurns; turn++) {
const compacted = await maybeCompact(client, model, messages, CONTEXT_LIMIT); // ← compaction organ
if (compacted !== messages) messages.splice(0, messages.length, ...compacted); // ← in place, so the caller's reference stays live
const res = await client.messages.create({ model, max_tokens: 2048, messages, tools, system }); // ← system prompt organ
messages.push({ role: 'assistant', content: res.content });
// ...print text...
if (res.stop_reason !== 'tool_use') return lastText; // ← loop's exit test
// run every tool_use, append one tool_result each, then loop
}
return lastText ? `${lastText}\nturn budget exhausted` : 'turn budget exhausted';
}
Six organs are visible in that one function. The for (… turn < maxTurns …) header is the budget — the loop stops itself instead of running forever. maybeCompact(...) at the top of every turn is the compaction organ. The system field threaded into messages.create is the new identity organ, and — crucially — it rides as the system argument, never pushed into messages, so compaction can never fold it away and it can never be quoted back as a "message." if (res.stop_reason !== 'tool_use') return is the loop's heart: keep going while the model asks for tools, return the moment it stops.
tutorial/06-harness/agent.ts supplies the other organs and does the assembly. The identity is a plain string, written once:
const SYSTEM = `You are a coding agent working in the user's repository at ${process.cwd()}.
Work in small steps: look before you act, prefer read_file/ls over guessing.
Use spawn_agent for exploration that would flood your context.
When the task is done, summarize what you changed and how you verified it.`;
The gate wraps every side effect in one place — gatedExecute runs decide() (deny → allow → ask) and only reaches executeTool on a pass:
async function gatedExecute(name, input, label = '') {
const verdict = decide(name, input);
console.log(`${label} ⚙ ${name}(${JSON.stringify(input)}) — ${verdict}`); // ← label = ' [sub]' inside a child
if (verdict === 'deny') return { content: 'denied by policy: …', isError: true };
if (verdict === 'ask' && !(await confirm(`${name}: … — run it?`)))
return { content: 'the user declined this action; …', isError: true };
return executeTool(name, input);
}
The sub-agent organ is runSubagent: a second runAgentLoop on a fresh, empty messages, a restricted tool set, and — the load-bearing detail — the restriction enforced at dispatch, not merely in the tool list the model sees:
const subTools = [toolRegistry.read_file!.spec, toolRegistry.bash!.spec];
const SUB_ALLOWED = new Set(['read_file', 'bash']); // no write_file, no spawn_agent
const label = ' [sub]'; // prefixes this child's 📏 meter and ⚙ verdict lines
return runAgentLoop(client, {
messages: [{ role: 'user', content: task }], // ← a clean desk
tools: subTools,
execute: async (name, input) =>
SUB_ALLOWED.has(name) ? gatedExecute(name, input, label)
: { content: `invalid input: tool "${name}" is not available to sub-agents`, isError: true },
label,
maxTurns: 10,
});
parentExecute routes spawn_agent into runSubagent (logging the dispatch, plus parent-context size before and after, to prove the child's exploration never entered the parent), and everything else into gatedExecute — so spawning itself never passes decide(); only the tool calls the child then makes do. Finally the top-level call snaps every organ together at once:
await runAgentLoop(client, {
model: MODEL, messages, tools, execute: parentExecute, label: '',
system: SYSTEM, // identity
maxTurns: Number(process.env.MAX_TURNS ?? 30), // budget
});
ANTHROPIC_API_KEY=… bun run tutorial/06-harness/agent.ts "create hello.txt saying hi"
A shortened transcript (the emoji come straight from the code):
📏 ~40 tokens of 8000
I'll create hello.txt in the current directory.
⚙ write_file({"path":"hello.txt","content":"hi"}) — ask
🔐 write_file: {"path":"hello.txt","content":"hi"} — run it? [y/N] y
📏 ~120 tokens of 8000
Created hello.txt with "hi". Verified by reading it back.
Ask it something that needs exploration and you'll see the sub-agent organ fire instead (the ↳ line is a plain dispatch log, not a ⚙ verdict — spawning is routed before decide(); the child's own tool calls still carry verdicts):
↳ spawn_agent({"task":"Explore this repo and report what it builds"}) — dispatching sub-agent
👁 parent context before spawn: ~90 tokens
[sub] 📏 ~60 tokens of 8000
[sub] ⚙ bash({"command":"ls"}) — allow
[sub] 📏 ~180 tokens of 8000
[sub] ⚙ read_file({"path":"package.json"}) — allow
[sub] 📏 ~450 tokens of 8000
It's the harness-tutorial workspace: staged TypeScript agents built on the Anthropic SDK…
👁 parent context after spawn: ~90 tokens (child's exploration never entered it)
The parent's token count is flat across the spawn: the child read files on its own clean desk and handed back a single answer. Let a long task run and you'll eventually see as the compaction organ folds old turns; hit the cap and the loop stops itself with turn budget exhausted.
Every organ in these ~90 lines is the skeleton of something the real harness does at scale. The map:
- Loop.
if (res.stop_reason !== 'tool_use') returnisqueryLoop'swhile (true)(claude-code/src/query.ts:307) and itsneedsFollowUpbranch — return when the model requested no tool, otherwise run tools and re-lap (claude-code/src/query.ts:1062). Deep read:/stories/loop/1/. - Tools.
executeTooldispatching a validated call mirrors the real validate-before-run chokepoint:tool.inputSchema.safeParse(input)runs before the tool body, and a bad call is bounced back to the model as an error instead of crashing (claude-code/src/services/tools/toolExecution.ts:615). Deep read:/stories/tools/1/. - Gate.
decide()'s deny → allow → ask is the real layered pipeline: a deny rule short-circuits todeny(claude-code/src/utils/permissions/permissions.ts:1171), and only anallowverdict ever reachesconst result = await tool.call(...)(claude-code/src/services/tools/toolExecution.ts:1207). Deep read:/stories/tools/2/. - Compaction.
maybeCompactfolding old turns aboveCONTEXT_LIMITis a hand-rolledgetAutoCompactThreshold— the real one triggers ateffectiveContextWindow − 13Kreserved buffer instead of a flat 8000 (claude-code/src/services/compact/autoCompact.ts:72). Deep read:/stories/context/1/. - Sub-agents. Our
spawn_agentruns a fresh inner loop with a restricted tool set — the real AgentTool has two flavors (a named specialist with its own fresh context, which ours models, plus an implicit fork that inherits the parent's context, which ours does not). The rule we enforce withSUB_ALLOWED+ nospawn_agent— a sub-agent cannot spawn its own children — shares its purpose with a real guard, not its mechanism: the actual recursion check is narrower, rejecting only fork-of-fork at call time — "Fork is not available inside a forked worker" (claude-code/src/tools/AgentTool/AgentTool.tsx:332-334) — because the Agent tool stays visible in a fork child's pool for cache reasons, while named/worker sub-agents get their tool pools assembled separately. Same goal (no runaway recursion), different mechanism. Deep read:/stories/multiagent/1/. - Budget.
maxTurnsis the simplest possible budget: a turn counter checked atnextTurnCount > maxTurns(claude-code/src/query.ts:1705). The real harness also runs a token-based self-pacing budget —checkTokenBudgetnudges the model to keep working until ~90% of an output-token target is spent (claude-code/src/query/tokenBudget.ts:45). Deep read:/stories/loop/2/. - System prompt. Sent once per call as
system, never stored inmessages— the same separation the real harness keeps between identity and transcript.
The composition itself — one build machine assembling these organs, page by page — is the subject of /stories/meta/1/.
This section is the demo above. Pull one organ and the run fails at a specific, characteristic step:
- No loop → pure talk. The model emits a
tool_use("I should run the tests") but nothing executes it; without thewhilethere is no second lap. It talks about the fix and stops. (Stage 0's failure: an API call that speaks but cannot act.) - No tools → fabrication. With nothing to call, the model can't touch the repo. It guesses the bug from memory and invents a patch — zero verified facts. (Stage 1's loop had nothing to loop over.)
- No gate → disaster. Mid-fix, a "let me clean up the old build"
rm -rflands with no brake. The deny verdict that would have焊死 it (claude-code/src/utils/permissions/permissions.ts:1171) is gone. (Stage 2's tools were live but ungoverned.) - No compaction → context overflow. Test output and file reads pile up until the window is full; the API returns prompt too long and the turn dies before reaching the patch. (Stage 3 could act but not survive a long transcript.)
- No sub-agents → context bloat. The "which of 40 files defines this?" exploration pours grep dumps and full file reads straight into the parent context, crowding out the actual fix. (Stage 4 could compact, but compaction is lossy — better not to inhale the noise at all.)
- No system prompt → wandering. With no identity or operating rules, the model treats "fix the failing test" as chit-chat, doesn't know it's a coding agent in this repo, and doesn't look before acting.
Assemble all seven and the run goes green: look → change → verify. That is where this thread ends, but the harness has more organs than six stages can hold — Skills (/stories/caps/1/), packaged procedures the model loads on demand instead of stuffing everything into one system prompt; Hooks (/stories/caps/2/), deterministic code at fixed points in the loop for policy that must never depend on the model choosing to comply; Memory (/stories/context/2/), state that persists across runs; richer stop conditions and a token budget (/stories/loop/2/) beyond "ran out of turns"; and deterministic workflows (/stories/multiagent/2/), pre-wired pipelines the model doesn't have to improvise. Each deep-dive page reads the real implementation and cites it line by line — which is the whole method of this knowledge base (/stories/meta/1/).
classDiagram
class agent_ts {
<<assembly · stage 06>>
+SYSTEM identity
+spawnAgentSpec
+gatedExecute(name,input)
+runSubagent(task)
+parentExecute(name,input)
}
class runAgentLoop {
<<loop.ts>>
+for turn lt maxTurns
+maybeCompact then callModel
+stopReason toolUse then run else return
}
class RunAgentLoopOptions {
+model messages tools
+execute()
+system maxTurns
}
class decide {
<<permission.ts>>
+deny allow ask
+confirm()
}
class maybeCompact {
<<compact.ts>>
+estimateTokens()
+fold old keep tail
}
class toolRegistry {
<<tools.ts>>
+read_file bash write_file
+executeTool validate
}
agent_ts ..> runAgentLoop : parent + each sub-agent
runAgentLoop ..> RunAgentLoopOptions : configured by
agent_ts ..> decide : gatedExecute wraps
runAgentLoop ..> maybeCompact : each turn
agent_ts ..> toolRegistry : tools + executeTool
agent_ts ..> agent_ts : spawn_agent → fresh loop 读法:Stage 06 的装配。agent.ts 把 loop.ts 的 runAgentLoop
同时用于父循环与每个子代理——同一个函数,换一份空 messages 与受限工具集就成了影分身。
runAgentLoop 由 RunAgentLoopOptions(带 system 身份与 maxTurns 预算)配置,
每轮先 maybeCompact(compact.ts)再调用模型,读 stop_reason 决定收工还是再跑一轮
(对照 claude-code/src/query.ts:307,1062)。gatedExecute 用 decide()
的 deny/allow/ask 把关每一次副作用(permissions.ts:1171),
toolRegistry 提供先校验后起锅的工具(toolExecution.ts:615),
spawn_agent 另起一个干净的内层循环——我们让子代理不许再唤子代理是派发层的一刀切规则,
真实实现里对应的守卫更窄:只在 fork 套 fork 时于调用时刻抛错(AgentTool.tsx:332-334),目的相同,机制不同。
七个器官装成一台机器,跑同一个任务:修复失败的测试。六个器官各配一个开关,默认全开。 关掉一个再「跑一遍」——时间线会走到缺了那个器官的那一步就崩,并告诉你怎么崩的: 没循环→纯聊天,没三档锁→rm -rf 灾难,没压缩→上下文溢出……全开才一路走到绿。
六个器官全开着。点「跑一遍」,或先关掉一个看它在哪一步崩。
这正是 装配(the harness):循环、工具、闸门、压缩、影分身、身份、预算——没有哪一个单独成事。
真正的智能体是这些器官的组合,而崩的也正是组合:抽掉一个,机器就在恰好用到它的那一步栽下去。
对照真实源码:while(true)(query.ts:307)、validate-before-run(toolExecution.ts:615)、
deny 档(permissions.ts:1171)、autocompact 阈值(autoCompact.ts:72)、maxTurns 上限(query.ts:1705)。
"分身不许再唤分身"是我们派发层的一刀切规则;真实实现里对应的守卫更窄——只在 fork 套 fork 时于调用时刻抛错(AgentTool.tsx:332-334),目的相同,机制不同。
The goal
Stage 6 introduces no new organ. It is the assembly. The five stages before it each grew one organ in isolation — the loop (loop.ts), real tools (tools.ts), the permission gate (permission.ts), compaction (compact.ts), and sub-agents (agent.ts's spawn_agent). The goal here is to wire all five into one running process, and to bolt on the last two organs the earlier stages never needed standalone: an explicit identity (a system prompt) and an explicit budget (a turn cap). The result is a mini Claude Code — small enough to read in one sitting, but with one of every organ a real agent harness has.
The whole point of the tutorial lives in this file: none of these organs is impressive alone. A loop with no gate is a chainsaw with no guard; a gate with no loop never gets asked twice. What makes an agent is the composition — and the composition is what breaks, organ by organ, when you pull a piece out. The demo above is exactly that experiment: flip one organ off, run "fix the failing test," and watch which step of the run falls over.
Build it
The keystone move is in tutorial/06-harness/loop.ts: the while-body from stage 4 is pulled out into a reusable function so it can run twice in one process — once for the top-level agent, once (with a different tool set and a fresh, empty messages) for each sub-agent:
export async function runAgentLoop(client: Anthropic, opts: RunAgentLoopOptions): Promise<string> {
const { model, messages, tools, execute, label, maxTurns = 20, system } = opts;
let lastText = '';
for (let turn = 0; turn < maxTurns; turn++) {
const compacted = await maybeCompact(client, model, messages, CONTEXT_LIMIT); // ← compaction organ
if (compacted !== messages) messages.splice(0, messages.length, ...compacted); // ← in place, so the caller's reference stays live
const res = await client.messages.create({ model, max_tokens: 2048, messages, tools, system }); // ← system prompt organ
messages.push({ role: 'assistant', content: res.content });
// ...print text...
if (res.stop_reason !== 'tool_use') return lastText; // ← loop's exit test
// run every tool_use, append one tool_result each, then loop
}
return lastText ? `${lastText}\nturn budget exhausted` : 'turn budget exhausted';
}
Six organs are visible in that one function. The for (… turn < maxTurns …) header is the budget — the loop stops itself instead of running forever. maybeCompact(...) at the top of every turn is the compaction organ. The system field threaded into messages.create is the new identity organ, and — crucially — it rides as the system argument, never pushed into messages, so compaction can never fold it away and it can never be quoted back as a "message." if (res.stop_reason !== 'tool_use') return is the loop's heart: keep going while the model asks for tools, return the moment it stops.
tutorial/06-harness/agent.ts supplies the other organs and does the assembly. The identity is a plain string, written once:
const SYSTEM = `You are a coding agent working in the user's repository at ${process.cwd()}.
Work in small steps: look before you act, prefer read_file/ls over guessing.
Use spawn_agent for exploration that would flood your context.
When the task is done, summarize what you changed and how you verified it.`;
The gate wraps every side effect in one place — gatedExecute runs decide() (deny → allow → ask) and only reaches executeTool on a pass:
async function gatedExecute(name, input, label = '') {
const verdict = decide(name, input);
console.log(`${label} ⚙ ${name}(${JSON.stringify(input)}) — ${verdict}`); // ← label = ' [sub]' inside a child
if (verdict === 'deny') return { content: 'denied by policy: …', isError: true };
if (verdict === 'ask' && !(await confirm(`${name}: … — run it?`)))
return { content: 'the user declined this action; …', isError: true };
return executeTool(name, input);
}
The sub-agent organ is runSubagent: a second runAgentLoop on a fresh, empty messages, a restricted tool set, and — the load-bearing detail — the restriction enforced at dispatch, not merely in the tool list the model sees:
const subTools = [toolRegistry.read_file!.spec, toolRegistry.bash!.spec];
const SUB_ALLOWED = new Set(['read_file', 'bash']); // no write_file, no spawn_agent
const label = ' [sub]'; // prefixes this child's 📏 meter and ⚙ verdict lines
return runAgentLoop(client, {
messages: [{ role: 'user', content: task }], // ← a clean desk
tools: subTools,
execute: async (name, input) =>
SUB_ALLOWED.has(name) ? gatedExecute(name, input, label)
: { content: `invalid input: tool "${name}" is not available to sub-agents`, isError: true },
label,
maxTurns: 10,
});
parentExecute routes spawn_agent into runSubagent (logging the dispatch, plus parent-context size before and after, to prove the child's exploration never entered the parent), and everything else into gatedExecute — so spawning itself never passes decide(); only the tool calls the child then makes do. Finally the top-level call snaps every organ together at once:
await runAgentLoop(client, {
model: MODEL, messages, tools, execute: parentExecute, label: '',
system: SYSTEM, // identity
maxTurns: Number(process.env.MAX_TURNS ?? 30), // budget
});
Run it
ANTHROPIC_API_KEY=… bun run tutorial/06-harness/agent.ts "create hello.txt saying hi"
A shortened transcript (the emoji come straight from the code):
📏 ~40 tokens of 8000
I'll create hello.txt in the current directory.
⚙ write_file({"path":"hello.txt","content":"hi"}) — ask
🔐 write_file: {"path":"hello.txt","content":"hi"} — run it? [y/N] y
📏 ~120 tokens of 8000
Created hello.txt with "hi". Verified by reading it back.
Ask it something that needs exploration and you'll see the sub-agent organ fire instead (the ↳ line is a plain dispatch log, not a ⚙ verdict — spawning is routed before decide(); the child's own tool calls still carry verdicts):
↳ spawn_agent({"task":"Explore this repo and report what it builds"}) — dispatching sub-agent
👁 parent context before spawn: ~90 tokens
[sub] 📏 ~60 tokens of 8000
[sub] ⚙ bash({"command":"ls"}) — allow
[sub] 📏 ~180 tokens of 8000
[sub] ⚙ read_file({"path":"package.json"}) — allow
[sub] 📏 ~450 tokens of 8000
It's the harness-tutorial workspace: staged TypeScript agents built on the Anthropic SDK…
👁 parent context after spawn: ~90 tokens (child's exploration never entered it)
The parent's token count is flat across the spawn: the child read files on its own clean desk and handed back a single answer. Let a long task run and you'll eventually see as the compaction organ folds old turns; hit the cap and the loop stops itself with turn budget exhausted.
对照 the real one
Every organ in these ~90 lines is the skeleton of something the real harness does at scale. The map:
- Loop.
if (res.stop_reason !== 'tool_use') returnisqueryLoop'swhile (true)(claude-code/src/query.ts:307) and itsneedsFollowUpbranch — return when the model requested no tool, otherwise run tools and re-lap (claude-code/src/query.ts:1062). Deep read:/stories/loop/1/. - Tools.
executeTooldispatching a validated call mirrors the real validate-before-run chokepoint:tool.inputSchema.safeParse(input)runs before the tool body, and a bad call is bounced back to the model as an error instead of crashing (claude-code/src/services/tools/toolExecution.ts:615). Deep read:/stories/tools/1/. - Gate.
decide()'s deny → allow → ask is the real layered pipeline: a deny rule short-circuits todeny(claude-code/src/utils/permissions/permissions.ts:1171), and only anallowverdict ever reachesconst result = await tool.call(...)(claude-code/src/services/tools/toolExecution.ts:1207). Deep read:/stories/tools/2/. - Compaction.
maybeCompactfolding old turns aboveCONTEXT_LIMITis a hand-rolledgetAutoCompactThreshold— the real one triggers ateffectiveContextWindow − 13Kreserved buffer instead of a flat 8000 (claude-code/src/services/compact/autoCompact.ts:72). Deep read:/stories/context/1/. - Sub-agents. Our
spawn_agentruns a fresh inner loop with a restricted tool set — the real AgentTool has two flavors (a named specialist with its own fresh context, which ours models, plus an implicit fork that inherits the parent's context, which ours does not). The rule we enforce withSUB_ALLOWED+ nospawn_agent— a sub-agent cannot spawn its own children — shares its purpose with a real guard, not its mechanism: the actual recursion check is narrower, rejecting only fork-of-fork at call time — "Fork is not available inside a forked worker" (claude-code/src/tools/AgentTool/AgentTool.tsx:332-334) — because the Agent tool stays visible in a fork child's pool for cache reasons, while named/worker sub-agents get their tool pools assembled separately. Same goal (no runaway recursion), different mechanism. Deep read:/stories/multiagent/1/. - Budget.
maxTurnsis the simplest possible budget: a turn counter checked atnextTurnCount > maxTurns(claude-code/src/query.ts:1705). The real harness also runs a token-based self-pacing budget —checkTokenBudgetnudges the model to keep working until ~90% of an output-token target is spent (claude-code/src/query/tokenBudget.ts:45). Deep read:/stories/loop/2/. - System prompt. Sent once per call as
system, never stored inmessages— the same separation the real harness keeps between identity and transcript.
The composition itself — one build machine assembling these organs, page by page — is the subject of /stories/meta/1/.
What breaks without it
This section is the demo above. Pull one organ and the run fails at a specific, characteristic step:
- No loop → pure talk. The model emits a
tool_use("I should run the tests") but nothing executes it; without thewhilethere is no second lap. It talks about the fix and stops. (Stage 0's failure: an API call that speaks but cannot act.) - No tools → fabrication. With nothing to call, the model can't touch the repo. It guesses the bug from memory and invents a patch — zero verified facts. (Stage 1's loop had nothing to loop over.)
- No gate → disaster. Mid-fix, a "let me clean up the old build"
rm -rflands with no brake. The deny verdict that would have焊死 it (claude-code/src/utils/permissions/permissions.ts:1171) is gone. (Stage 2's tools were live but ungoverned.) - No compaction → context overflow. Test output and file reads pile up until the window is full; the API returns prompt too long and the turn dies before reaching the patch. (Stage 3 could act but not survive a long transcript.)
- No sub-agents → context bloat. The "which of 40 files defines this?" exploration pours grep dumps and full file reads straight into the parent context, crowding out the actual fix. (Stage 4 could compact, but compaction is lossy — better not to inhale the noise at all.)
- No system prompt → wandering. With no identity or operating rules, the model treats "fix the failing test" as chit-chat, doesn't know it's a coding agent in this repo, and doesn't look before acting.
Assemble all seven and the run goes green: look → change → verify. That is where this thread ends, but the harness has more organs than six stages can hold — Skills (/stories/caps/1/), packaged procedures the model loads on demand instead of stuffing everything into one system prompt; Hooks (/stories/caps/2/), deterministic code at fixed points in the loop for policy that must never depend on the model choosing to comply; Memory (/stories/context/2/), state that persists across runs; richer stop conditions and a token budget (/stories/loop/2/) beyond "ran out of turns"; and deterministic workflows (/stories/multiagent/2/), pre-wired pipelines the model doesn't have to improvise. Each deep-dive page reads the real implementation and cites it line by line — which is the whole method of this knowledge base (/stories/meta/1/).
来源 · Source citations
- [1]
claude-code/src/query.ts:307 - [2]
claude-code/src/query.ts:1062 - [3]
claude-code/src/query.ts:1705 - [4]
claude-code/src/services/tools/toolExecution.ts:615 - [5]
claude-code/src/services/tools/toolExecution.ts:1207 - [6]
claude-code/src/utils/permissions/permissions.ts:1171 - [7]
claude-code/src/services/compact/autoCompact.ts:72 - [8]
claude-code/src/tools/AgentTool/AgentTool.tsx:332-334 - [9]
claude-code/src/query/tokenBudget.ts:45