research

humanlayer/humanlayer — a code-level field guide

Jul 14, 2026

Target repos (cloned to ~/Developer/reference-repos/):

  • humanlayer/humanlayer — the monorepo behind CodeLayer / the "advanced context engineering" (ACE) toolchain. Contains the daemon (hld/, Go), the CLI + MCP server (hlyr/, TS), a Go SDK that drives Claude Code (claudecode-go/), and — the crown jewels — a full .claude/ command + subagent library.
  • humanlayer/12-factor-agents — Dex Horthy's manifesto (11k+ stars). The essays are well-known; the transferable signal is the code shape they imply.

Why this repo is signal

Most "agent framework" repos give you an abstraction to import. HumanLayer instead ships the actual operating system they use to build their own product with Claude Code — the slash commands, the subagent definitions, the daemon that gates every tool call behind a human, and the SDK that launches Claude as a subprocess. Two things make it top-0.1%-power-user material:

  1. The .claude/ directory is a complete, battle-tested Claude Code workflow — research → plan → implement → validate → handoff, with parallel documentarian subagents and a "thoughts" persistence layer. You can steal these files almost verbatim.
  2. The daemon (hld) is a reference implementation of durable, human-gated, resumable agent sessions — the thing everyone hand-waves about ("human in the loop", "pause/resume") built out in real Go + SQLite + a JSON-RPC event bus.

Everything below cites a real file. Line numbers are from the shallow clone on 2026-07-08; treat them as ±a few lines if the repo moved.


1. The research→plan→implement→validate pipeline (the ACE core)

This is the single highest-ticket steal. The workflow lives entirely in .claude/commands/*.md as slash commands, each with YAML frontmatter that pins a model.

1.1 Phase separation is enforced by different commands with different jobs

  • research_codebase.md (model: opus) — only documents the codebase as-is.
  • create_plan.md (model: opus) — interactive, skeptical plan authoring.
  • implement_plan.md — executes an approved plan, checkbox by checkbox.
  • validate_plan.md — a separate agent verifies the implementation against the plan.

The research command opens with an almost aggressive constraint (.claude/commands/research_codebase.md:10-17):

## CRITICAL: YOUR ONLY JOB IS TO DOCUMENT AND EXPLAIN THE CODEBASE AS IT EXISTS TODAY
- DO NOT suggest improvements or changes unless the user explicitly asks for them
- DO NOT perform root cause analysis unless the user explicitly asks for them
...
- ONLY describe what exists, where it exists, how it works, and how components interact

Why it matters: separating "understand" from "propose" from "do" from "check" keeps each phase's context window small and its objective unambiguous — a direct application of 12-factor #10 (small, focused agents). A research agent that stays a documentarian doesn't burn its context arguing with itself about the fix.

How to apply: copy these four command files into your own .claude/commands/. Even solo, running /research_codebase before /create_plan before /implement_plan beats one mega-prompt, because each phase produces a durable artifact the next phase (or a fresh session) reads.

1.2 "Read mentioned files FULLY yourself before spawning subagents"

Repeated verbatim across research_codebase.md, create_plan.md, implement_plan.md:

research_codebase.md:30-34 — "Read these files yourself in the main context before spawning any sub-tasks... Use the Read tool WITHOUT limit/offset parameters to read entire files."

create_plan.md:45-47 — "CRITICAL: DO NOT spawn sub-tasks before reading these files yourself in the main context. NEVER read files partially."

Why it matters: the orchestrator must hold the load-bearing context itself; subagents return summaries, and a summary of a file you never read is a game of telephone. This is the "orchestrator touches the work twice" idea made concrete.

1.3 Plans split success criteria into automated vs manual verification

The plan template (create_plan.md:225-241, and the guidelines at :345-376) forces every phase to carry two checklists:

#### Automated Verification:
- [ ] Migration applies cleanly: `make migrate`
- [ ] Unit tests pass: `make test-component`
- [ ] Type checking passes: `npm run typecheck`

#### Manual Verification:
- [ ] Feature works as expected when tested via UI
- [ ] Performance is acceptable under load

**Implementation Note**: After completing this phase and all automated
verification passes, pause here for manual confirmation from the human...

And a hard rule (create_plan.md:338-343): "No Open Questions in Final Plan" — if a question surfaces during planning, STOP and resolve it; the plan must be complete and actionable before implementation begins.

Why it matters: this is exactly the "closed-loop per-phase checklist" pattern — each phase has its own gate, automated checks are things an execution agent can literally run, and manual checks are explicitly fenced off as the human's job. It maps 1:1 onto your unattended-run doctrine (pasted proof, per-phase validation).

How to apply: make your /create_plan (or plan template) demand the automated/manual split, and phrase automated criteria as copy-pasteable commands (make check test, go test ./...) so the implementer and validator both just run them.

1.4 Implementation is checkbox-driven and resumable

implement_plan.md:78-83:

"If the plan has existing checkmarks: Trust that completed work is done. Pick up from the first unchecked item."

And it edits the plan file itself as it goes (implement_plan.md:48-49: "Check off completed items in the plan file itself using Edit"). The plan .md doubles as durable progress state — a fresh session can resume mid-plan by reading the checkmarks.

Why it matters: the plan document is the execution state (12-factor #5, "unify execution state and business state"). No separate task tracker; the artifact and the state are the same file, which survives compaction and session death.


2. The documentarian subagent library (.claude/agents/)

Six subagents, each a .md with frontmatter pinning model: and a restricted toolset. These are the workers the commands above spawn in parallel.

Agent tools model job
codebase-locator Grep, Glob, LS sonnet find WHERE code lives, never read it
codebase-analyzer Read, Grep, Glob, LS sonnet explain HOW code works, with file:line
codebase-pattern-finder Grep, Glob, Read, LS sonnet find existing patterns to model after
thoughts-locator / thoughts-analyzer same, over the thoughts/ doc store
web-search-researcher external docs, must return links

2.1 Tool restriction as behavior enforcement

codebase-locator.md:4tools: Grep, Glob, LS. It literally cannot Read files, which is why its instructions (:98) can say "Don't read file contents - Just report locations" and mean it. The locator is a "Super Grep/Glob/LS tool" (:3).

Why it matters: you enforce the division of labor by removing the tools, not by asking nicely. A locator with no Read tool can't wander into analysis and blow its context. This is the same "enforcement-shaped, not prose-shaped" principle from your own doctrine, applied to subagent design.

2.2 The "documentarian, not critic" refrain

Every analysis agent repeats a long "What NOT to Do" block (codebase-analyzer.md:124-138, codebase-pattern-finder.md:209-221):

- Don't identify bugs, issues, or potential problems
- Don't comment on performance or efficiency
- Don't suggest alternative implementations
## REMEMBER: You are a documentarian, not a critic or consultant

Why it matters: unconstrained, a strong model will start refactoring in its head and pollute a "just tell me how it works" report with opinions. Constraining the output type keeps the artifact reusable by the next phase.

2.3 Pattern-finder returns working code with file:line, not prose

codebase-pattern-finder.md:60-168 mandates output that includes the actual snippet, a **Found in**: src/api/users.js:45-67 header, "Key aspects" bullets, and the test pattern for the same feature.

How to apply: define your own codebase-locator / codebase-analyzer / codebase-pattern-finder trio. Pin them to a cheap-but-capable model (sonnet here), give locator only search tools, and have the orchestrator (opus) fan them out in parallel then synthesize. This is a portable, model-agnostic version of the "Explore subagent" pattern.


3. Context compaction via handoff documents

When a session runs long, HumanLayer doesn't rely on auto-compaction — they write an explicit handoff and start fresh.

create_handoff.md:6-8:

"write a handoff document to hand off your work to another agent in a new session... The goal is to compact and summarize your context without losing any of the key details."

The template (create_handoff.md:43-64) has fixed sections: Task(s) (with status per task, and which plan phase you're on), Critical References (2-3 most important file paths), Recent changes (in file:line syntax), Learnings (root causes, gotchas — "someone picking up your work should know"), Artifacts, Action Items & Next Steps.

Crucially (create_handoff.md:95): "avoid excessive code snippets... Prefer using /path/to/file.ext:line references that an agent can follow later." The handoff stores pointers, not content — the next agent re-reads the live files (which may have changed), so the handoff can never go stale on code.

resume_handoff.md:16 closes the loop: on resume, read the handoff AND its linked plan/research docs FULLY, do NOT use a sub-agent to read these critical files, then verify the handoff's claims against current state before acting (:166-171: "Never assume handoff state matches current state").

Why it matters: this is a manual, lossless, inspectable alternative to context compaction. The handoff is a small, structured, pointer-based summary; the actual context is rehydrated from disk on demand. It's the durable version of your own handoff/RUN-STATE discipline.

How to apply: add /create_handoff and /resume_handoff commands. Trigger a handoff at ~70% context instead of letting the model auto-compact. Store handoffs by ticket with timestamped filenames (ENG-XXXX/YYYY-MM-DD_HH-MM-SS_desc.md) so /resume_handoff ENG-1234 can auto-pick the most recent (resume_handoff.md:22-26).


4. The "thoughts" persistence layer

Every command reads from and writes to a thoughts/ directory and calls humanlayer thoughts sync after writing (e.g. research_codebase.md:166, create_plan.md:281). It's a git-backed knowledge store with a searchable hard-link mirror.

  • Research → thoughts/shared/research/YYYY-MM-DD-ENG-XXXX-desc.md
  • Plans → thoughts/shared/plans/...
  • Handoffs → thoughts/shared/handoffs/ENG-XXXX/...
  • Every doc carries YAML frontmatter: date, researcher, git_commit, branch, repository, topic, tags, status, last_updated (research_codebase.md:99-111). Metadata is generated by a script: hack/spec_metadata.sh (allow-listed in .claude/settings.json).

The searchable mirror is a hard-link tree; the commands carry explicit path-rewriting rules (research_codebase.md:200-207): strip only searchable/ from any path, preserve every other segment.

The CLI implementing this lives in hlyr/src/commands/thoughts/ (sync.ts, init.ts, config.ts, status.ts, plus a profile/ subtree).

Why it matters: research and plans become first-class, versioned, greppable artifacts with provenance (commit + branch baked in). Six months later you can find "what did we know about X in January" — and the frontmatter tells you the exact commit the research was true for. This is your GBrain instinct, but scoped to a single repo and wired directly into the agent loop.

How to apply: even without their CLI, adopt the convention: a thoughts/ (or ai/) dir, dated-and-ticketed filenames, YAML frontmatter with git_commit/branch, and a post-write git commit. Have a spec_metadata.sh emit the frontmatter so the model never hand-fills it wrong.


5. Human-in-the-loop: gate every tool call through an MCP request_permission tool

This is the mechanism behind the whole product, and it's small enough to steal. HumanLayer runs Claude Code with --permission-prompt-tool pointed at a custom MCP tool that blocks until a human decides.

5.1 The approval MCP server (hlyr/src/mcp.ts)

The server exposes exactly one tool, request_permission (hlyr/src/mcp.ts:48-62):

name: 'request_permission',
inputSchema: {
  properties: {
    tool_name: { type: 'string' },
    input: { type: 'object' },
    tool_use_id: { type: 'string' },
  },
  required: ['tool_name', 'input', 'tool_use_id'],
},

The handler creates an approval in the daemon, then polls once a second until the human resolves it (hlyr/src/mcp.ts:98-139):

const createResponse = await daemonClient.createApproval(sessionId, toolName, input, toolUseId)
// ...
while (polling) {
  const approval = await daemonClient.getApproval(approvalId)
  if (approval.status !== 'pending') {
    approved = approval.status === 'approved'
    comment = approval.comment || ''
    polling = false
  } else {
    await new Promise(resolve => setTimeout(resolve, 1000))
  }
}

And it returns the exact JSON shape Claude Code's SDK expects for a permission decision (hlyr/src/mcp.ts:141-167):

// denied:
{ behavior: 'deny', message: comment || 'Request denied by human reviewer' }
// approved:
{ behavior: 'allow', updatedInput: input }

Note updatedInput on allow — the human can edit the tool input on the way through, not just approve/reject.

Why it matters: this is the entire "human in the loop" pattern in ~120 lines. Claude Code natively supports handing every tool call to an MCP tool; that tool can do anything — block on a human, check a policy, rewrite the input. The session ID comes from HUMANLAYER_SESSION_ID in the env (mcp.ts:82), so approvals are correlated to the right session.

How to apply: if you want a policy/approval gate on an agent, write a tiny MCP server exposing request_permission and launch Claude with --permission-prompt-tool mcp__yourserver__request_permission. Return {behavior:'allow'|'deny', ...}. You now have a chokepoint for auto-approve-safe-tools / block-dangerous-ones / ask-a-human — without touching the model.

5.2 The daemon side: state machine + event bus (hld/PROTOCOL.md)

The daemon is JSON-RPC 2.0 over a Unix socket (~/.humanlayer/daemon.sock, perms 0600, line-delimited JSON — hld/PROTOCOL.md:8-13). Its session state machine (PROTOCOL.md:385-391):

starting → running → completed | failed
                   ↘ waiting_input

Approval status is a separate axis (PROTOCOL.md:393-399): NULL | pending | approved | denied | resolved.

Approvals correlate to tool calls by tool_use_id, and the conversation is persisted as an event log (PROTOCOL.md:240-256) — each row: sequence, event_type (message|tool_call|tool_result|system), role, tool_id, tool_name, tool_input_json, tool_result_for_id, is_completed, approval_status, approval_id.

Clients (the WUI/TUI) get live updates via a long-poll Subscribe method with event types new_approval | approval_resolved | session_status_changed and a 30s heartbeat (PROTOCOL.md:320-374).

Why it matters: this is 12-factor #5/#6/#8 in production Go: execution state lives in a queryable SQLite event log, sessions pause on waiting_input and resume, and an event bus fans status out to any number of UIs. The approval and the tool call are joined by tool_use_id, so a human decision in the WUI flows back to the exact blocked tool call in the MCP server's poll loop.

5.2a Daemon internals worth stealing (verified in the Go source)

The mcp.ts poll-loop in §5.1 is the CLI-side client; the daemon side is richer and each piece is independently reusable:

  • 9-state session machine, not 3 (hld/session/types.go:16-29): draft, starting, running, completed, failed, interrupting, interrupted, waiting_input, discarded. The non-obvious states — waiting_input (blocked on a human), interrupting/interrupted (shutting down but resumable), draft (not launched) — are exactly the states a durable, steerable agent needs. Mirrored in the DB layer (hld/store/store.go:274-285).
  • The gate is a Go channel, not a busy poll (hld/mcp/server.go:166-201): the daemon's own MCP server registers decisionChan := make(chan ApprovalDecision, 1) keyed by tool_use_id in a sync.Map, blocks on select { case decision := <-decisionChan: ... case <-ctx.Done(): }, and a listenForApprovalDecisions goroutine (server.go:222-260) subscribes to the event bus and pushes the human's decision into the right channel. Denies return {behavior:"deny", message: comment}; approves return {behavior:"allow", updatedInput: input}.
  • Auto-approval is the same code path, not a bypass (hld/approval/manager.go:44-66): DangerouslySkipPermissions and AutoAcceptEdits (for edit tools) just make CreateApproval return status=approved immediately with a comment like "Auto-accepted (auto-accept mode enabled)" — still written to the DB, still published as an event. Uniform audit trail regardless of mode.
  • Time-boxed skip-permissions with a watchdog (hld/session/permission_monitor.go): DangerouslySkipPermissions carries an ExpiresAt, and a 30s ticker goroutine disables expired windows and emits session_settings_changed with reason:"expired". The pattern: never trust the client to turn "skip all approvals" back off — expire it server-side.
  • Crash recovery via orphaned-session reconciliation (hld/daemon/daemon.go:341-388): on startup the daemon sweeps sessions still marked running/waiting_input/starting (whose real subprocess died with the previous daemon) and marks them failed — but leaves interrupting/interrupted/completed/failed alone so they can still be resumed. State is marked honestly, not lost.
  • Continue = interrupt-then-fork, building a session tree (hld/session/manager.go:1433-1507): ContinueSession SIGINTs a still-running parent, Wait()s for it to actually exit, then launches a new daemon session (new session_id/run_id, linked by parent_session_id) that forks the underlying Claude conversation (SessionID: parent.ClaudeSessionID, ForkSession: true). You get a branching tree in the DB even though the CLI only knows linear resume/fork. Requires the parent to have captured a claude_session_id.
  • The daemon auto-injects its own approval MCP server (hld/session/manager.go:195-263): every launched session gets codelayer MCP wired in with HUMANLAYER_SESSION_ID + HUMANLAYER_DAEMON_SOCKET smuggled through the MCP subprocess's env, and PermissionPromptTool defaulted to mcp__codelayer__request_permission. That env-var smuggling is how a separate MCP process talks back to the right session's approval queue.
  • SIGINT vs SIGKILL is a deliberate API (claudecode-go/client.go:445-459): Interrupt() sends SIGINT (graceful, stays resumable); Kill() sends SIGKILL. The daemon's "stop but keep resumable" depends on this distinction.

5.2b The persistence + bus tricks (verified)

  • One conversation_events table reconstructs the whole transcript (hld/store/sqlite.go:132-167): every message/tool_call/tool_result is one row with a sequence column, is_completed, approval_status, approval_id, and parent_tool_use_id. Ordered replay by sequence.
  • Partial indexes make "what's waiting on a human" cheapCREATE INDEX idx_conversation_pending_approvals ON conversation_events(approval_status) WHERE approval_status='pending' (sqlite.go:165-167) and the same trick on the approvals table (sqlite.go:200-218, with status ... CHECK (status IN ('pending','approved','denied'))). WAL + foreign_keys pragmas at sqlite.go:46-53.
  • Hand-rolled additive migrations for a local-first DB (hld/store/sqlite.go:236-303+): version tracked in a schema_version table; each migration checks pragma_table_info for column existence before ALTER TABLE ADD COLUMN (SQLite has no ADD COLUMN IF NOT EXISTS), so it runs safely on both fresh and years-old on-disk databases. 13+ migrations, e.g. adding parent_tool_use_id (sub-agent tracking) and dangerously_skip_permissions.
  • Drop-on-full event bus (hld/bus/events.go:82-125): Publish does select { case sub.Channel <- event: default: /* drop, log "slow subscriber" */ } over per-subscriber buffered channels (size 100), with context-driven auto-unsubscribe. Dropping rather than blocking is critical because of the synchronous approval gate in §5.2a — one slow WUI subscriber must never stall approval delivery to a live Claude process.
  • JSON-RPC over a 0600 Unix socket, no auth (hld/rpc/server.go, PROTOCOL.md:9-13): the whole control plane is secured by filesystem permissions alone; handlers are a plain map[string]HandlerFunc; the socket scanner uses the same 10MB buffer as the SDK (rpc/server.go comment: "10MB buffer to match claudecode-go") to avoid re-hitting the large-tool-output truncation bug at a second layer.
  • ClaudeSession interface for testability (hld/session/claudecode_wrapper.go:7-64): a thin //go:generate mockgen interface (Interrupt/Kill/GetID/Wait/GetEvents) wraps the concrete SDK session so the daemon's orchestration logic is unit-testable without spawning real claude subprocesses.

5.3 Operational discipline baked into hld/CLAUDE.md

The daemon's own CLAUDE.md tells the agent: "You cannot run this process, you cannot restart it. If you make changes, you must ask the user to rebuild it." — and hands over SQLite one-liners for introspection (sqlite3 ~/.humanlayer/daemon.db ".schema") plus nc -U SOCKET for raw RPC testing.

How to apply: for any long-lived process an agent works on but must not bounce, put a "you cannot restart this; here's how to inspect it" block in the local CLAUDE.md. The debug.md command (below) operationalizes it.


6. Driving Claude Code as a subprocess: the Go SDK (claudecode-go/)

claudecode-go is a clean reference for "launch Claude Code programmatically and stream its events" — useful for any orchestration layer.

6.1 The config surface (claudecode-go/types.go)

SessionConfig (types.go:46-73) is the whole knob-set worth knowing:

type SessionConfig struct {
    Query string
    // Session management
    SessionID   string // If set, resumes this session
    ForkSession bool   // If true with SessionID, forks instead of resuming
    // Optional
    Model                 Model          // opus | sonnet | haiku
    OutputFormat          OutputFormat   // text | json | stream-json
    MCPConfig             *MCPConfig
    PermissionPromptTool  string
    WorkingDir            string
    MaxTurns              int
    SystemPrompt          string
    AppendSystemPrompt    string
    AllowedTools          []string
    DisallowedTools       []string
    ...
}

SessionID + ForkSession is the key insight: resume continues a session; fork branches it — you can explore multiple continuations from one checkpoint (types.go:50-52).

6.2 CLI flags are the real API (claudecode-go/client.go:183-260+)

buildArgs shows exactly how each config maps to a claude CLI flag:

if config.SessionID != "" {
    args = append(args, "--resume", config.SessionID)
    if config.ForkSession { args = append(args, "--fork-session") }
}
// stream-json requires --verbose
if config.OutputFormat == OutputStreamJSON && !config.Verbose {
    args = append(args, "--verbose")
}

Two gotchas worth stealing: stream-json output silently requires --verbose (client.go:204-206), and MCP config is written to a temp file and passed via --mcp-config <file> rather than inline (client.go:220-236).

6.3 The streaming parse loop (claudecode-go/client.go:462-543)

Claude Code emits newline-delimited JSON events. The parser:

scanner := bufio.NewScanner(stdout)
scanner.Buffer(make([]byte, 0), 10*1024*1024) // 10MB max line size

with a comment explaining why (client.go:464-466): "This prevents buffer overflow when Claude returns large file contents." Each line unmarshals into a StreamEvent; a failed unmarshal is logged and dropped, not fatal (client.go:490-494); the final type:"result" event carries total_cost_usd, num_turns, usage, permission_denials (types.go:88-102, client.go:502-518).

StreamEvent also has ParentToolUseID (types.go:83) — how sub-task events are correlated to the parent tool call, which is how the daemon builds the parent/child session tree.

Why it matters: if you ever wrap Claude Code in your own harness (like your pxpipe/OpenClaw layer), this file is the map: launch flags, the --verbose trap, the 10MB scanner buffer (real bug-preventer for big file reads), tolerant per-line parsing, and the result event as the cost/usage sink.

How to apply: when you shell out to claude, use --output-format stream-json --verbose, read line-by-line with a big buffer, drop-and-log unparseable lines, and treat the result event as your billing/telemetry record.


7. Autonomous loops: the "Ralph" commands + worktree isolation

.claude/commands/ralph_*.md are self-driving loops over a Linear backlog. ralph_impl.md (model: sonnet):

  1. If no ticket given, fetch top-10 Linear items in "ready for dev", select the highest-priority SMALL/XS issue; if none, EXIT IMMEDIATELY (ralph_impl.md:14-15).
  2. Require a linked implementation plan; if no plan exists, move the ticket back to "ready for spec" and EXIT (ralph_impl.md:26).
  3. Create an isolated git worktree (hack/create_worktree.sh ENG-XXXX BRANCH).
  4. Launch a fresh nightly session inside the worktree to implement, commit, open a PR, and comment the PR link back on the ticket (ralph_impl.md:31).

ralph_research.md is the same shape for tickets in "research needed" → produces a research doc, attaches it, moves the ticket to "research in review".

7.1 Worktree creation is a safety gate (hack/create_worktree.sh)

create_worktree.sh:94-101: after creating the worktree it runs make setup, and if setup fails it destroys the worktree and refuses:

if ! make setup; then
    git worktree remove --force "$WORKTREE_PATH"
    git branch -D "$WORKTREE_NAME" 2>/dev/null || true
    echo "❌ Not allowed to create worktree from a branch that isn't passing setup."
    exit 1
fi

It also copies .claude/ into the worktree (:85-88) so the subagent inherits the same commands/agents, and initializes thoughts in the new tree (:120-133).

Why it matters: this is exactly your fan-out doctrine — bounded scope (only SMALL/XS), hard exit conditions (no plan → stop; setup fails → destroy), and worktree isolation so parallel agents never collide. The "refuse to start from a broken base" check means an autonomous run can't build on sand.

How to apply: for any nightly//loop factory, gate each unit of work behind (a) a size filter, (b) a "prerequisite artifact exists or exit" check, and (c) a worktree that self-destructs if the base doesn't pass setup/check. Copy .claude/ into the worktree so the spawned session has your full command library.


8. debug.md — an investigation agent that doesn't burn primary context

.claude/commands/debug.md is a read-only forensic command. Its framing (debug.md:7): "a way to bootstrap a debugging session without using the primary window's context." It spawns three parallel tasks — logs, DB state, git state (debug.md:76-111) — and knows the exact forensic surface: MCP logs at ~/.humanlayer/logs/mcp-claude-approvals-*.log, the SQLite daemon DB, ps aux | grep hld. It ends with a fixed Debug Report template (What's Wrong / Evidence / Root Cause / Next Steps) and an explicit "No file editing — Pure investigation only" (debug.md:167).

Why it matters: debugging is context-expensive and mostly disposable. Firing it as a subagent (or a fresh window) keeps the wrong-turns out of the main session, and the fixed report template means the parent gets a clean conclusion, not a transcript.

How to apply: make a /debug command that is read-only, enumerates your app's exact log/DB/process locations, fans out parallel investigators, and returns a fixed-format report. Never let the debugger edit.


9. Small but sharp conventions worth copying

  • Thinking budget in settings.claude/settings.json: "env": { "MAX_THINKING_TOKENS": "32000" }. They dial thinking up globally for the repo, and pin model: opus on the reasoning-heavy commands (research, plan) in frontmatter.
  • Priority-tagged TODOs (humanlayer/CLAUDE.md): TODO(0) never-merge critical → TODO(4) questions, plus PERF. Greppable severity; an agent can be told "fix all TODO(1)".
  • make-first verification (create_plan.md:325): success criteria must prefer make -C humanlayer-wui check over raw cd ... && bun run fmt, so the same command works for human, implementer, and validator.
  • Commit hygiene as a command (commit.md): git add specific files, never -A/.; never add Claude attribution / Co-Authored-By (commit.md:33-38); present the commit plan and ask before committing.
  • 12-factor #3 "own your context window" (12-factor-agents/content/factor-03-own-your-context-window.md:120-140): don't accept the default message array — serialize your thread into a custom (often XML-ish) format:
    def event_to_prompt(event): return f"<{event.type}>\n{data}\n</{event.type}>"
    def thread_to_prompt(thread): return '\n\n'.join(event_to_prompt(e) for e in thread.events)
    
    with the explicit tip (factor-03:230): "Consider hiding errors and failed calls from context window once they are resolved."
  • 12-factor #12 "stateless reducer" — the whole agent is while True: next = DetermineNextStep(thread.serialize()); apply(next); thread.append(result) (12-factor-agents/workshops/2025-07-16/walkthrough/03-agent.py:14-40). The LLM is a pure function state → next_action; you own the loop and the state.
  • HITL as a tool call, not a special mode (factor-07-contact-humans-with-tools.md:29-46): model outputs intent: "request_human_input" (or done_for_now) as structured output; your loop breaks, persists thread, notifies a human, and resumes on a webhook. Same abstraction scales to Agent→Agent.

Top ~6 highest-ticket steals

  1. The four-command ACE pipelineresearch_codebase.mdcreate_plan.mdimplement_plan.mdvalidate_plan.md (.claude/commands/). Copy them. The discipline (documentarian research, plan with automated+manual gates and zero open questions, checkbox-resumable implementation, independent validation) is worth more than any framework. Files are near-verbatim reusable.

  2. The documentarian subagent triocodebase-locator (search-tools-only), codebase-analyzer, codebase-pattern-finder (.claude/agents/). Enforce role by removing tools, pin to a cheap model, fan out in parallel, synthesize in the orchestrator. Portable model-agnostic version of the Explore subagent.

  3. request_permission MCP gate (hlyr/src/mcp.ts:48-167) — ~120 lines to put a policy/human chokepoint on every tool call via --permission-prompt-tool, returning {behavior:'allow'|'deny', updatedInput}. The updatedInput field (human edits the call) is the non-obvious win.

  4. Handoff-doc context compaction (create_handoff.md / resume_handoff.md) — pointer-based (file.ext:line, never code blobs), fixed sections, re-verified on resume. A lossless, inspectable replacement for auto-compaction; trigger it at ~70% context.

  5. The hld durable-session model (hld/PROTOCOL.md) — session state machine (starting/running/waiting_input/completed/failed) + approval axis (pending/approved/denied) + SQLite event log keyed by tool_use_id + long-poll event bus. The reference design for pause/resume/human-gated agents (12-factor #5/#6/#8 in real Go).

  6. The Go SDK's launch+stream loop (claudecode-go/client.go) — the exact recipe for wrapping Claude Code: --output-format stream-json requires --verbose, 10MB scanner buffer for big file reads, drop-and-log bad lines, SessionID+ForkSession for resume-vs-branch, result event as the cost/usage sink. Plus the thoughts/ convention (dated+ticketed filenames, YAML frontmatter carrying git_commit/branch) as the artifact-provenance layer around it.


Provenance / caveats

  • All file paths and line numbers verified by reading the files in the 2026-07-08 shallow clone at ~/Developer/reference-repos/humanlayer and ~/Developer/reference-repos/12-factor-agents. Line numbers may drift slightly with upstream changes.
  • §1-4, §6-9 and §5.1 were read directly by me. §5.2 (hld/PROTOCOL.md, hld/CLAUDE.md) I read directly. §5.2a/§5.2b (the hld/session/, hld/mcp/, hld/approval/, hld/store/, hld/bus/, hld/rpc/, hld/daemon/ Go internals with line numbers) were read and cited by a delegated sub-agent that explored the Go source; I have not personally re-opened every one of those files, but the citations are specific line ranges, not inferences, and are consistent with the protocol docs I read directly. Spot-check before quoting a line number as gospel.
  • 12-factor-agents referenced .promptx/personas/* (developer/reviewer/multiplan-manager personas) in its CLAUDE.md, but those files are not committed to the repo (they're generated by promptx init), so I could not read them. [UNVERIFIED] beyond the persona names/roles listed in 12-factor-agents/CLAUDE.md.
  • The 12-factor essays also ship a hands-on BAML walkthrough under 12-factor-agents/workshops/2025-07-16/walkthrough/ (chapters 0-7: hello-world → agent loop → tools → tests → human-tools → context serialization) if you want the runnable version of factors 1-8.