Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

11 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

harness code

🌐 语言 / Language: 中文 · English

A full-featured terminal coding agent, built in TypeScript from the reverse-engineered Claude Code technical docs in docs/. It speaks the Anthropic Messages API over streaming SSE, so it runs against any /v1/messages endpoint — verified against a new-api proxy serving gpt-5.5 / gpt-5.4-mini / mimo-v2.5.

It is a single ~205 KB bundled CLI: a ReAct agent loop, 13 built-in tools, a permission pipeline with a hand-written Bash AST safety analyzer, context compaction, session persistence + resume, hooks, plan mode, an interactive Ink REPL with a context progress bar, pinned todo panel, non-blocking input, free terminal scrolling, and MCP stdio integration.


一个功能完整的终端编码 agent,用 TypeScript 基于 docs/ 中逆向还原的 Claude Code 技术文档构建。它通过流式 SSE 对接 Anthropic Messages API,因此能跑在任何 /v1/messages 端点上——已在提供 gpt-5.5 / gpt-5.4-mini / mimo-v2.5 的 new-api 代理上验证。

它是一个约 205 KB 的单 bundle CLI:ReAct agent 循环、13 个内置工具、带手写 Bash AST 安全分析器的权限流水线、上下文压缩、会话持久化与 resume、hooks、plan 模式、带上下文进度条/固定 todo 面板/非阻塞输入/终端自由滚动的交互式 Ink REPL,以及 MCP stdio 集成。


Table of contents


目录


Quick start

npm install
npm run build      # → dist/main.js (single ESM bundle)

# Headless: one prompt, stream result to stdout, exit.
node dist/main.js --print \
  --api-key "$HARNESS_API_KEY" \
  --base-url "https://saturday.sankuai.com" \
  --model gpt-5.5 \
  "List the TypeScript files in this repo"

# Interactive REPL.
node dist/main.js --api-key "$HARNESS_API_KEY" --base-url "https://saturday.sankuai.com"

Install globally:

npm link
harness-code           # interactive
harness-code -p "..."  # headless

Credentials resolve automatically from a config file (see Configuration), so after harness-code --init-config you can run harness-code with no flags.


快速开始

npm install
npm run build      # → dist/main.js(单个 ESM bundle)

# Headless:单次提问,流式输出到 stdout,退出。
node dist/main.js --print \
  --api-key "$HARNESS_API_KEY" \
  --base-url "https://saturday.sankuai.com" \
  --model gpt-5.5 \
  "列出这个仓库里的 TypeScript 文件"

# 交互式 REPL。
node dist/main.js --api-key "$HARNESS_API_KEY" --base-url "https://saturday.sankuai.com"

全局安装:

npm link
harness-code           # 交互
harness-code -p "..."  # headless

凭证会从配置文件自动解析(见 配置),所以 harness-code --init-config 之后可以不带任何 flag 直接运行 harness-code


Configuration

Config is resolved by src/cli/config.ts (resolveConfig) with this priority chain (highest wins):

  1. CLI flags--api-key, --base-url, --model, --small-model, --max-turns
  2. HARNESS_* env varsHARNESS_API_KEY, HARNESS_BASE_URL, HARNESS_MODEL, HARNESS_SMALL_MODEL, HARNESS_MAX_OUTPUT_TOKENS, HARNESS_AUTH_TOKEN, API_TIMEOUT_MS
  3. Config file~/.harness-code/config.json (user) merged with <cwd>/.harness-code/config.json (project overrides user). Holds apiKey, baseURL, model, smallModel, maxOutputTokens, and a models catalog for /model switching.
  4. ~/.claude/settings.json + project .claude/settings.json + .claude/settings.local.json
  5. Built-in defaultsbaseURL=https://saturday.sankuai.com, model=gpt-5.5, smallModel=gpt-5.4-mini, maxOutputTokens=8192

Note on ANTHROPIC_*: harness code intentionally does not read ANTHROPIC_BASE_URL / ANTHROPIC_API_KEY / etc. Doing so would silently route requests to the developer's Claude Code shell env (a different proxy). Use HARNESS_* or the config file instead. See the ANTHROPIC_* env vars are NOT read test in tests/unit/config.test.ts.

Generate a config template:

harness-code --init-config   # writes ./.harness-code/config.json

Permission mode (CLI): --permission-mode default|auto|bypassPermissions or --dangerously-skip-permissions. At runtime in the REPL use /bypass (see Slash commands).


配置

配置由 src/cli/config.tsresolveConfig 解析,优先级链(高者覆盖低者):

  1. CLI flag —— --api-key--base-url--model--small-model--max-turns
  2. HARNESS_* 环境变量 —— HARNESS_API_KEYHARNESS_BASE_URLHARNESS_MODELHARNESS_SMALL_MODELHARNESS_MAX_OUTPUT_TOKENSHARNESS_AUTH_TOKENAPI_TIMEOUT_MS
  3. 配置文件 —— ~/.harness-code/config.json(用户级)与 <cwd>/.harness-code/config.json(项目级覆盖用户级)。存放 apiKeybaseURLmodelsmallModelmaxOutputTokens,以及用于 /model 切换的 models 目录。
  4. ~/.claude/settings.json + 项目 .claude/settings.json + .claude/settings.local.json
  5. 内置默认 —— baseURL=https://saturday.sankuai.commodel=gpt-5.5smallModel=gpt-5.4-minimaxOutputTokens=8192

关于 ANTHROPIC_*:harness code 故意不读 ANTHROPIC_BASE_URL / ANTHROPIC_API_KEY 等。否则会静默地把请求路由到开发者 shell 里 Claude Code 的环境变量(另一个代理)。请用 HARNESS_* 或配置文件。详见 tests/unit/config.test.ts 中的 ANTHROPIC_* env vars are NOT read 测试。

生成配置模板:

harness-code --init-config   # 写入 ./.harness-code/config.json

权限模式(CLI):--permission-mode default|auto|bypassPermissions--dangerously-skip-permissions。REPL 运行时用 /bypass(见 斜杠命令)。


Architecture

                          ┌─────────────────────────────────────────┐
   stdin / args           │  src/main.tsx  (Commander entrypoint)    │
        │                  │  resolve config → build engine           │
        ▼                  └───────────────┬─────────────────────────┘
   ┌────────────────┐                      │  --print? → headless : REPL
   │ entrypoints/   │◀─────────────────────┘
   │  headless.ts   │   src/ink/App.tsx (Ink/React REPL)
   │                │   transcript, spinner, progress bar, plan UI,
   │                │   permission prompts, model selector
   └───────┬────────┘
           │
           ▼
   ┌────────────────────────────────────────────────────────────────┐
   │ src/QueryEngine.ts  (session orchestrator)                       │
   │  • messages[] (history)   • modelManager (runtime /model)        │
   │  • usageTracker          • readFileState (read-before-write)     │
   │  • systemPrompt (memoized) • hooks registry • permCtx (mutable) │
   │  • sessionId (JSONL persistence)                                │
   └───────┬────────────────────────────────────────────────────────┘
           │  submitMessage(prompt, callbacks)
           ▼
   ┌────────────────────────────────────────────────────────────────┐
   │ src/query.ts  — the agent while-loop (ReAct)                     │
   │   while true:                                                    │
   │     1. shouldAutoCompact? → summarize older turns               │
   │     2. client.callModel(stream) → accumulate content blocks     │
   │     3. no tool_use & stop=end_turn → return 'completed'          │
   │     4. runTools(tool_use blocks) → tool_result messages          │
   │     5. append assistant + tool_result, continue                  │
   │   error recovery: prompt_too_long / max_output / abort          │
   └───────┬────────────────────────────────────────────────────────┘
           │  per tool_use block
           ▼
   src/query/runTools.ts (executeOne)
     validateInput → PreToolUse hook → canUseTool → tool.call → PostToolUse hook
Layer Module Purpose
Entrypoint src/main.tsx Commander flags → config → engine → headless or REPL
Headless src/entrypoints/headless.ts Single-shot run; text or stream-json (NDJSON) output
REPL src/ink/App.tsx Ink/React TUI: transcript, spinner, progress bar, plan/permission prompts
Session src/QueryEngine.ts Multi-turn history, usage, file-state cache, model/hooks/session state
Agent loop src/query.ts, src/query/runTools.ts, src/query/abort.ts ReAct loop, tool execution, abort synthesis
Tools src/tools.ts, src/tools/ 13 built-in tools + buildTool() factory
Permissions src/permissions/, src/utils/permissions/ Decision pipeline, rule matching, classifier
Bash safety src/utils/bash/ Recursive-descent parser + fail-closed AST traverser
Context src/context.ts System prompt, CLAUDE.md, git status, memory
Memory src/memdir/ Auto-memory dir, frontmatter files, MEMORY.md index
Compaction src/services/compact/compact.ts Auto/manual summarization + circuit breaker
API client src/services/api/ Anthropic-compatible streaming SSE + callOnce
MCP src/services/mcp/client.ts stdio client, mcp__server__tool injection
Hooks src/services/hooks/ PreToolUse/PostToolUse/Stop/… matchers + runner
Skills src/skills/ SKILL.md loading, slash-command parsing
Session store src/services/session/ JSONL transcripts + meta sidecar
Memory extraction src/services/extractMemories/ LLM-assisted /memory save

架构

                          ┌─────────────────────────────────────────┐
   stdin / 参数            │  src/main.tsx  (Commander 入口)          │
        │                  │  解析配置 → 构建 engine                   │
        ▼                  └───────────────┬─────────────────────────┘
   ┌────────────────┐                      │  --print? → headless : REPL
   │ entrypoints/   │◀─────────────────────┘
   │  headless.ts   │   src/ink/App.tsx (Ink/React REPL)
   │                │   转录、spinner、进度条、plan UI、
   │                │   权限提示、模型选择器
   └───────┬────────┘
           │
           ▼
   ┌────────────────────────────────────────────────────────────────┐
   │ src/QueryEngine.ts  (会话编排器)                                 │
   │  • messages[] (历史)    • modelManager (运行时 /model)           │
   │  • usageTracker         • readFileState (写前读检查)              │
   │  • systemPrompt (缓存)  • hooks 注册表 • permCtx (可变)          │
   │  • sessionId (JSONL 持久化)                                    │
   └───────┬────────────────────────────────────────────────────────┘
           │  submitMessage(prompt, callbacks)
           ▼
   ┌────────────────────────────────────────────────────────────────┐
   │ src/query.ts  —— agent while 循环 (ReAct)                       │
   │   while true:                                                    │
   │     1. shouldAutoCompact? → 摘要旧轮次                           │
   │     2. client.callModel(stream) → 累积内容块                     │
   │     3. 无 tool_use 且 stop=end_turn → return 'completed'         │
   │     4. runTools(tool_use 块) → tool_result 消息                  │
   │     5. 追加 assistant + tool_result, 继续                         │
   │   错误恢复: prompt_too_long / max_output / abort               │
   └───────┬────────────────────────────────────────────────────────┘
           │  每个 tool_use 块
           ▼
   src/query/runTools.ts (executeOne)
     validateInput → PreToolUse hook → canUseTool → tool.call → PostToolUse hook
模块 职责
入口 src/main.tsx Commander flag → 配置 → engine → headless 或 REPL
Headless src/entrypoints/headless.ts 单次运行;textstream-json (NDJSON) 输出
REPL src/ink/App.tsx Ink/React TUI:转录、spinner、进度条、plan/权限提示
会话 src/QueryEngine.ts 多轮历史、usage、文件状态缓存、model/hooks/session 状态
Agent 循环 src/query.tssrc/query/runTools.tssrc/query/abort.ts ReAct 循环、工具执行、abort 合成
工具 src/tools.tssrc/tools/ 13 个内置工具 + buildTool() 工厂
权限 src/permissions/src/utils/permissions/ 决策流水线、规则匹配、分类器
Bash 安全 src/utils/bash/ 递归下降解析器 + fail-closed AST 遍历器
上下文 src/context.ts 系统提示、CLAUDE.md、git status、记忆
记忆 src/memdir/ auto-memory 目录、frontmatter 文件、MEMORY.md 索引
压缩 src/services/compact/compact.ts 自动/手动摘要 + 熔断器
API 客户端 src/services/api/ Anthropic 兼容流式 SSE + callOnce
MCP src/services/mcp/client.ts stdio 客户端、mcp__server__tool 注入
Hooks src/services/hooks/ PreToolUse/PostToolUse/Stop/… matcher + runner
Skills src/skills/ SKILL.md 加载、斜杠命令解析
会话存储 src/services/session/ JSONL 转录 + meta 副文件
记忆提取 src/services/extractMemories/ LLM 辅助的 /memory save

The agent loop

src/query.ts is the core while(true) ReAct loop. Per turn:

  1. Turn guardturnCount >= maxTurnsreason: 'max_turns'.
  2. Auto-compact checkdeps.autoCompact(messages) if configured; replaces history with a summary when over the threshold (see Compaction).
  3. Inject queued inputdeps.injectMessages() appends any user messages typed while the agent was running (non-blocking input), after the previous turn's tool_result (pairing intact).
  4. Streaming model callclient.callModel({model, messages, system, tools, max_tokens}, {onEvent, signal}). Events are parsed by stream.ts (message_start, content_block_*, message_delta, message_stop, error); a StreamAccumulator reconstructs the final message and usage.
  5. Append assistant turn — thinking/redacted blocks are dropped before re-sending (proxy quirks; see assistantBlocksForNextTurn in types.ts).
  6. Collect tool_use blocks — if none and stop_reason ∈ {end_turn, stop_sequence}reason: 'completed'.
  7. Run toolsrunTools(tool_use_blocks) partitions into concurrency-safe (parallel) and unsafe (serial) groups, executes each through executeOne, returns tool_result messages.
  8. Loop — append assistant + tool_result messages, continue.

Error recovery (query.ts)

  • prompt_too_long (HTTP 413) → attempt one compaction, then retry; if compaction fails, reason: 'prompt_too_long'.
  • max_output_tokens → escalate max_tokens to 64 000, then up to 3 mid-thought recoveries by injecting "Continue from where you left off." as a user turn.
  • Abort (RequestAbortedError, from engine.interrupt() / Esc / Ctrl+C) → synthesize missing tool_result blocks for any orphaned tool_use via abort.ts and return reason: 'aborted_streaming' / 'aborted_tools'. Partial assistant content already accumulated is preserved.

The QueryExitReason union: completed | aborted_streaming | aborted_tools | max_turns | prompt_too_long | error.


Agent 循环

src/query.ts 是核心 while(true) ReAct 循环。每轮:

  1. 轮数守卫 —— turnCount >= maxTurnsreason: 'max_turns'
  2. 自动压缩检查 —— 若配置了 deps.autoCompact(messages),超阈值时用摘要替换历史(见 压缩)。
  3. 注入排队输入 —— deps.injectMessages() 追加 agent 运行期间用户键入的消息(非阻塞输入),在上一轮 tool_result 之后(配对完整)。
  4. 流式模型调用 —— client.callModel({model, messages, system, tools, max_tokens}, {onEvent, signal})。事件由 stream.ts 解析(message_startcontent_block_*message_deltamessage_stoperror);StreamAccumulator 重建最终消息和 usage
  5. 追加 assistant 轮 —— thinking/redacted 块在重发前丢弃(代理怪癖;见 types.tsassistantBlocksForNextTurn)。
  6. 收集 tool_use 块 —— 若无且 stop_reason ∈ {end_turn, stop_sequence}reason: 'completed'
  7. 执行工具 —— runTools(tool_use_blocks) 按并发安全(并行)和不安全(串行)分组,经 executeOne 执行,返回 tool_result 消息。
  8. 循环 —— 追加 assistant + tool_result 消息,继续。

错误恢复(query.ts

  • prompt_too_long (HTTP 413) → 尝试一次压缩后重试;压缩失败则 reason: 'prompt_too_long'
  • max_output_tokens → 把 max_tokens 升级到 64 000,然后最多 3 次「中途恢复」:注入 "Continue from where you left off." 作为 user 轮。
  • AbortRequestAbortedError,来自 engine.interrupt() / Esc / Ctrl+C)→ 通过 abort.ts 为孤立的 tool_use 合成缺失的 tool_result 块,返回 reason: 'aborted_streaming' / 'aborted_tools'。已累积的部分 assistant 内容会保留。

QueryExitReason 联合类型:completed | aborted_streaming | aborted_tools | max_turns | prompt_too_long | error


Tool system

Built by the buildTool() factory in src/Tool.ts, which injects fail-closed defaults: isConcurrencySafe → false, isReadOnly → false, isDestructive → false, checkPermissions → passthrough. Tools may declare a Zod inputSchema (built-ins) or a raw inputJSONSchema (MCP); the factory converts Zod → JSON schema for the API (supporting both Zod v3 and v4).

13 built-in tools (src/tools.ts):

FileReadTool · FileEditTool · FileWriteTool · NotebookEditTool · BashTool · GlobTool · GrepTool · TodoWriteTool · AskUserQuestionTool · WebFetchTool · AgentTool (sub-agent) · SkillTool · ExitPlanModeTool.

executeOne in runTools.ts runs, per block:

validateInput (Zod) → validateInput hook → PreToolUse hooks
  → canUseTool (permission pipeline) → tool.call() → PostToolUse hooks
  → mapToolResultToToolResultBlockParam

Result-size budgeting truncates oversized results (each tool sets maxResultSizeChars).

Concurrency model

runTools partitions tool_use blocks into:

  • safe (isConcurrencySafe(input) === true) → Promise.all parallel,
  • unsafe → serial in order,
  • unknown tools → immediate Unknown tool error result.

Output is re-sorted to original block order for determinism.

Key tool behaviors

  • Read-before-writeFileEdit/FileWrite consult readFileState (a FileStateCache in src/utils/file/readFileState.ts) and require a prior full FileRead + mtime freshness check, else error.
  • AgentTool (src/tools/AgentTool/AgentTool.ts) spawns an in-process sub-agent via query() with isolated history and bridged permission context; result extracted by getFinalText().
  • ExitPlanModeTool — blocks on a user-approval promise; see Plan mode.

工具系统

src/Tool.tsbuildTool() 工厂构建,注入 fail-closed 默认值isConcurrencySafe → falseisReadOnly → falseisDestructive → falsecheckPermissions → passthrough。工具可声明 Zod inputSchema(内置)或原始 inputJSONSchema(MCP);工厂把 Zod → JSON schema 转换供 API 使用(支持 Zod v3 和 v4)。

13 个内置工具(src/tools.ts):

FileReadTool · FileEditTool · FileWriteTool · NotebookEditTool · BashTool · GlobTool · GrepTool · TodoWriteTool · AskUserQuestionTool · WebFetchTool · AgentTool(子 agent)· SkillTool · ExitPlanModeTool

runTools.tsexecuteOne 对每个块执行:

validateInput (Zod) → validateInput hook → PreToolUse hooks
  → canUseTool (权限流水线) → tool.call() → PostToolUse hooks
  → mapToolResultToToolResultBlockParam

结果大小预算会截断过大的结果(每个工具设 maxResultSizeChars)。

并发模型

runToolstool_use 块分为:

  • 安全isConcurrencySafe(input) === true)→ Promise.all 并行,
  • 不安全 → 按顺序串行,
  • 未知工具 → 立即返回 Unknown tool 错误结果。

输出按原始块顺序重排,保证确定性。

关键工具行为

  • 写前读 —— FileEdit/FileWrite 查询 readFileStatesrc/utils/file/readFileState.tsFileStateCache),要求先有完整 FileRead + mtime 新鲜度检查,否则报错。
  • AgentToolsrc/tools/AgentTool/AgentTool.ts)通过 query() 派生进程内子 agent,隔离历史、桥接权限上下文;结果由 getFinalText() 提取。
  • ExitPlanModeTool —— 阻塞在用户审批 promise 上;见 Plan 模式

Permission system & Bash safety

hasPermissionsToUseTool(tool, input, context, permCtx) runs:

  1. Tool-level deny ruledeny (bypass-immune).
  2. Tool-level ask ruleask.
  3. tool.checkPermissions(input)passthrough | allow | deny | ask.
  4. Safety-path check (.git/, .claude/, .vscode/, shell configs) → ask (bypass-immune).
  5. Content rules — shell-rule matching for Bash (exact/prefix/wildcard), path matching for file tools.
  6. bypassPermissions modeallow.
  7. Read-only toolsallow (in both auto and default modes — so pwd/ls/FileRead never prompt).
  8. passthrough → ask (or deny if avoidPrompts, e.g. headless).

createCanUseTool (src/permissions/canUseTool.ts) resolves ask to allow/deny:

  • default mode → calls the onAsk callback (the REPL's [y]/[n] prompt; headless → deny),
  • auto mode → runs the AI classifier first (src/permissions/classifier.ts),
  • bypassPermissions → allow (already resolved in the pipeline).

The classifier (classifyYoloAction) calls the small model with the tool name + input and a safety prompt; fail-closed (error/timeout/non-JSON → shouldBlock: true). Results are cached by (toolName, inputHash).

Permission modes

Mode Read-only Write tools How to enter
default allow ask → interactive [y]/[n] (default)
auto allow AI classifier decides --permission-mode auto
bypassPermissions allow allow (except bypass-immune safety paths) --dangerously-skip-permissions, --permission-mode bypassPermissions, or /bypass at runtime

The permCtx object is shared by reference between QueryEngine and createCanUseTool, so engine.setPermissionMode('bypassPermissions') takes effect on the next tool check — that's how /bypass works live.

Bash AST safety (src/utils/bash/)

BashTool doesn't regex-match commands — it parses them. A hand-written lexer (lexer.ts) feeds a recursive-descent parser (bashParser.ts) producing an AST (ast.ts, types.ts). The traverser is fail-closed:

  • Rejects IFS/PS4/declare -n assignments, unquoted heredocs with expansions, standalone command substitution.
  • Any node type it can't fully analyze → too-complex → ask user.
  • bashCommandTouchesSafetyPath() flags commands touching .git/.claude/.vscode/shell configs (bypass-immune).
  • Unicode-whitespace rejection, shell-rule matching (exact/prefix/wildcard) in shellRuleMatching.ts.

权限系统与 Bash 安全

hasPermissionsToUseTool(tool, input, context, permCtx) 执行:

  1. 工具级 deny 规则deny(bypass 免疫)。
  2. 工具级 ask 规则ask
  3. tool.checkPermissions(input)passthrough | allow | deny | ask
  4. 保护路径检查.git/.claude/.vscode/、shell 配置)→ ask(bypass 免疫)。
  5. 内容规则 —— Bash 的 shell 规则匹配(exact/prefix/wildcard),文件工具的路径匹配。
  6. bypassPermissions 模式allow
  7. 只读工具allowauto default 模式都放行——所以 pwd/ls/FileRead 从不询问)。
  8. passthrough → ask(或 avoidPromptsdeny,如 headless)。

createCanUseToolsrc/permissions/canUseTool.ts)把 ask 解析为 allow/deny

  • default 模式 → 调 onAsk 回调(REPL 的 [y]/[n] 提示;headless → deny),
  • auto 模式 → 先跑 AI 分类器(src/permissions/classifier.ts),
  • bypassPermissions → allow(已在流水线里解析)。

分类器(classifyYoloAction)用工具名 + 输入 + 安全 prompt 调小模型;fail-closed(错误/超时/非 JSON → shouldBlock: true)。结果按 (toolName, inputHash) 缓存。

权限模式

模式 只读 写工具 如何进入
default allow ask → 交互 [y]/[n] (默认)
auto allow AI 分类器决定 --permission-mode auto
bypassPermissions allow allow(bypass 免疫的保护路径除外) --dangerously-skip-permissions--permission-mode bypassPermissions,或运行时 /bypass

permCtx 对象在 QueryEnginecreateCanUseTool按引用共享,所以 engine.setPermissionMode('bypassPermissions') 在下一次工具检查时即时生效——这就是 /bypass 能实时切换的原理。

Bash AST 安全(src/utils/bash/

BashTool 不用正则匹配命令——而是解析它们。手写 lexer(lexer.ts)喂给递归下降解析器(bashParser.ts),生成 AST(ast.tstypes.ts)。遍历器 fail-closed

  • 拒绝 IFS/PS4/declare -n 赋值、带展开的未引用 heredoc、独立命令替换。
  • 任何无法完整分析的节点类型 → too-complex → 询问用户。
  • bashCommandTouchesSafetyPath() 标记触碰 .git/.claude/.vscode/shell 配置的命令(bypass 免疫)。
  • Unicode 空格拒绝;shell 规则匹配(exact/prefix/wildcard)在 shellRuleMatching.ts

Context, memory & compaction

System prompt (src/context.ts)

fetchSystemPromptParts() assembles (memoized once per session):

  • tool descriptions (tool.prompt()) + behavioral rules,
  • custom / appended system prompts (--system-prompt, --append-system-prompt),
  • CLAUDE.md memory — discovered by walking the current dir and all parents up to the filesystem root, plus ~/.claude/CLAUDE.md and --add-dir directories, deduped via realpath() (skip when CLAUDE_CODE_DISABLE_CLAUDE_MDS=1),
  • git status summary (git status --short, git log --oneline -n 5, branch, user) capped at 2000 chars,
  • the auto-memory prompt.

Auto-memory directory (src/memdir/)

~/.claude/projects/<sanitized-git-root>/memory/ — one frontmatter markdown file per fact (name, description, metadata.type ∈ {user, feedback, project, reference}), indexed by MEMORY.md (one line per memory). Shared across all worktrees of a repo (findCanonicalGitRoot).

/memory shows loaded memories; /memory save runs extractMemories — calls the small model with the recent transcript + existing index, parses proposed memories as JSON, dedupes by name, writes new frontmatter files and rebuilds the index.

  • Auto: shouldAutoCompact(messages, contextWindow) fires when tokens exceed contextWindow − 20K (summary budget) − 13K (buffer). Default contextWindow = 400_000 (DEFAULT_CONTEXT_WINDOW), so the threshold is ~367K tokens. Triggered at the top of each turn in the loop.
  • Manual: /compactengine.compactNow() bypasses the threshold.
  • Process: keep the most recent 2 messages; summarize older turns via client.callOnce (small model) into a <context_compaction> block; replace history.
  • Circuit breaker: 3 consecutive failures stop further compaction attempts (resetCompactionState() resets).
  • Hooks: PreCompact/PostCompact fire before/after (observe-only).
  • Token estimation is rough: chars / 4 (estimateTokens).

上下文、记忆与压缩

系统提示(src/context.ts

fetchSystemPromptParts() 组装(每会话缓存一次):

  • 工具描述(tool.prompt())+ 行为规范,
  • 自定义/追加系统提示(--system-prompt--append-system-prompt),
  • CLAUDE.md 记忆——遍历当前目录及所有父目录直到文件系统根,加上 ~/.claude/CLAUDE.md--add-dir 目录,经 realpath() 去重(CLAUDE_CODE_DISABLE_CLAUDE_MDS=1 时跳过),
  • git status 摘要(git status --shortgit log --oneline -n 5、分支、用户),截断到 2000 字符,
  • auto-memory prompt。

Auto-memory 目录(src/memdir/

~/.claude/projects/<sanitized-git-root>/memory/——每个事实一个 frontmatter markdown 文件(namedescriptionmetadata.type ∈ {user, feedback, project, reference}),由 MEMORY.md 索引(每条记忆一行)。同一 repo 的所有 worktree 共享(findCanonicalGitRoot)。

/memory 显示已加载记忆;/memory save 运行 extractMemories——用最近转录 + 现有索引调小模型,把提议的记忆解析为 JSON,按 name 去重,写新 frontmatter 文件并重建索引。

  • 自动shouldAutoCompact(messages, contextWindow) 在 token 超过 contextWindow − 20K (摘要预算) − 13K (缓冲) 时触发。默认 contextWindow = 400_000DEFAULT_CONTEXT_WINDOW),阈值约 367K token。在循环每轮顶部检查。
  • 手动/compactengine.compactNow() 绕过阈值。
  • 过程:保留最近 2 条消息;用 client.callOnce(小模型)把旧轮次摘要成 <context_compaction> 块;替换历史。
  • 熔断器:连续 3 次失败停止后续压缩尝试(resetCompactionState() 重置)。
  • HooksPreCompact/PostCompact 在前后触发(仅观察)。
  • Token 估算是粗略的:chars / 4estimateTokens)。

Session persistence & resume

src/services/session/ stores every conversation as JSONL (one Message per line) under ~/.harness-code/projects/<sanitized-cwd>/<sessionId>.jsonl, plus a <sessionId>.meta.json sidecar (id, cwd, model, createdAt, updatedAt, messageCount, summary).

  • QueryEngine creates a session at construction (or resumes when given sessionId).
  • After each submitMessage, appendMessages incrementally appends only the new messages from that turn — crash-safe (partial writes don't corrupt earlier lines).
  • --resume [id] (CLI): no id → resume the latest session for the cwd; with id → load that one.
  • /sessions, /resume, /export commands; REPL hydrates the transcript UI on resume.
  • Persistence can be disabled (disableSessionPersistence, used by tests).

会话持久化与 resume

src/services/session/ 把每个对话存为 JSONL(每行一条 Message),位于 ~/.harness-code/projects/<sanitized-cwd>/<sessionId>.jsonl,外加 <sessionId>.meta.json 副文件(idcwdmodelcreatedAtupdatedAtmessageCountsummary)。

  • QueryEngine 构造时创建会话(或给定 sessionId 时 resume)。
  • 每次 submitMessage 后,appendMessages 增量追加该轮新消息——崩溃安全(部分写不破坏已有行)。
  • --resume [id](CLI):无 id → resume 当前 cwd 最新会话;有 id → 加载指定会话。
  • /sessions/resume/export 命令;REPL 在 resume 时水合转录 UI。
  • 可禁用持久化(disableSessionPersistence,测试用)。

Hooks

src/services/hooks/ implements docs §05.5. Configured in settings.json under a hooks field: { <Event>: HookMatcher[] } where HookMatcher = { matcher?: string, hooks: HookCommand[] }.

Events: PreToolUse, PostToolUse, UserPromptSubmit, SessionStart, SessionEnd, Stop, PreCompact, PostCompact.

Two hook types in v1:

  • command — shell subprocess; stdin = JSON(input), stdout parsed as { decision, reason }.
  • function — in-process callback.

(http / prompt / agent types are declared for forward-compat but logged + skipped.)

PreToolUse is the only event that can short-circuit: { decision: 'block' } denies the tool (with reason), { decision: 'approve' } skips the permission ask path. All other events are observe-only. Fail-closed-but-not-blocking: hook errors/timeouts are logged and treated as no-decision.

Wiring: runTools.executeOne runs PreToolUse before canUseTool and PostToolUse after tool.call; QueryEngine fires SessionStart/UserPromptSubmit/Stop/SessionEnd; compactConversation fires PreCompact/PostCompact. The hooks registry is built from settings.hooks via loader.ts and merged with in-process function hooks.


Hooks

src/services/hooks/ 实现文档 §05.5。在 settings.jsonhooks 字段配置:{ <Event>: HookMatcher[] },其中 HookMatcher = { matcher?: string, hooks: HookCommand[] }

事件:PreToolUsePostToolUseUserPromptSubmitSessionStartSessionEndStopPreCompactPostCompact

v1 两种类型:

  • command——shell 子进程;stdin = JSON(input),stdout 解析为 { decision, reason }
  • function——进程内回调。

http / prompt / agent 类型已声明供前向兼容,但会被 log 后跳过。)

PreToolUse 是唯一能短路的事件:{ decision: 'block' } 拒绝工具(带 reason),{ decision: 'approve' } 跳过权限 ask 路径。其他事件仅观察。Fail-closed 但不阻塞:hook 错误/超时会 log 并视为无 decision。

接线:runTools.executeOnecanUseTool 前跑 PreToolUse,在 tool.call 后跑 PostToolUseQueryEngine 触发 SessionStart/UserPromptSubmit/Stop/SessionEndcompactConversation 触发 PreCompact/PostCompact。hooks 注册表由 loader.tssettings.hooks 构建,与进程内 function hook 合并。


Plan mode

Plan mode lets the agent research read-only, then present a plan for user approval before writing.

  • --plan (CLI) starts in plan mode; /plan toggles it at runtime.
  • In plan mode, QueryEngine.toolsForCurrentMode() exposes only read-only tools + ExitPlanModeTool to the model (write tools are filtered out). isReadOnly is wrapped in try/catch (input-dependent classifiers like BashTool fail-closed to non-read-only with no input).
  • When the model calls ExitPlanMode({plan}), the tool blocks on a promise resolved by the REPL's [y]/[n] prompt (onPlanPresented callback). On approve, planMode flips to false → the next turn uses the full tool set. On reject, the tool returns an error result and the agent revises.
  • The approval handler is a module-level singleton (setPlanApprovalHandler) registered by the engine at construction.

Plan 模式

Plan 模式让 agent 只读研究,然后写代码前提交 plan 供审批。

  • --plan(CLI)启动即进 plan 模式;/plan 运行时切换。
  • plan 模式下,QueryEngine.toolsForCurrentMode() 只向模型暴露只读工具 + ExitPlanModeTool(写工具被过滤)。isReadOnly 包在 try/catch 里(像 BashTool 这种依赖输入的分类器,无输入时 fail-closed 为非只读)。
  • 模型调用 ExitPlanMode({plan}) 时,工具阻塞在 REPL [y]/[n] 提示(onPlanPresented 回调)解析的 promise 上。approve → planMode 翻为 false → 下一轮用全工具集。reject → 工具返回 error result,agent 修订。
  • 审批 handler 是模块级单例(setPlanApprovalHandler),engine 构造时注册。

MCP integration

src/services/mcp/client.ts is a real MCP stdio client (using @modelcontextprotocol/sdk). It:

  • Spawns servers from settings.mcpServers, connects with a timeout (MCP_TIMEOUT, default 30s), batches at concurrency 3.
  • Discovers tools and injects them into the registry as mcp__<server>__<tool>, preserving annotations (readOnlyHint, destructiveHint, openWorldHint).
  • Proxies tool calls back to the server at runtime.
  • disconnectAll() for graceful shutdown.

v1 scope: stdio only. SSE / HTTP / WebSocket transports are not implemented (per scope decision).


MCP 集成

src/services/mcp/client.ts 是真正的 MCP stdio 客户端(用 @modelcontextprotocol/sdk)。它:

  • settings.mcpServers 派生服务器进程,带超时连接(MCP_TIMEOUT,默认 30s),并发 3 批量。
  • 发现工具并注入注册表为 mcp__<server>__<tool>,保留 annotation(readOnlyHintdestructiveHintopenWorldHint)。
  • 运行时代理工具调用回服务器。
  • disconnectAll() 优雅关闭。

v1 范围:仅 stdio。SSE / HTTP / WebSocket 传输未实现(按范围决策)。


Skills

src/skills/loadSkillsDir.ts loads SKILL.md files from skill directories. parseSlashCommand() turns /foo bar into {name: 'foo', args: 'bar'}. SkillTool lets the agent invoke skills; /skills lists them. The REPL also accepts bare exit/quit (no slash) to exit.


Skills

src/skills/loadSkillsDir.ts 从 skill 目录加载 SKILL.md 文件。parseSlashCommand()/foo bar 转成 {name: 'foo', args: 'bar'}SkillTool 让 agent 调用 skill;/skills 列出它们。REPL 也接受裸 exit/quit(无斜杠)退出。


The REPL UI

src/ink/App.tsx is an Ink/React TUI. Notable behaviors:

  • Startup banner — a 4-line half-block ASCII-art harness code logo, rendered via <Static> so it persists in the scrollback and survives /resume remounts.
  • <Static> transcript — the conversation history (and banner) render through Ink's <Static> component, which writes each item to the terminal scrollback once and never redraws it. Ink's logUpdate only redraws the fixed bottom region (cwd hint, spinner, input, footer, panels), so the terminal can be scrolled freely — no snapping to top/bottom.
  • Transcript colors — user green, assistant cyan, tool calls dim; each tool line shows its target (file path / command / todo count), not just the tool name.
  • Spinner — braille ⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏ (80ms) with an activity label (thinking / running BashTool / writing).
  • Context progress barsrc/ink/barGlyph.ts renders [███████▓▒░░░░░░░] ctx: 12.3k/400k (3%) · gpt-5.5 · default · $0.0028 with a flowing shine and a traveling dot (always animating, even at 0%); color shifts cyan → yellow → red as usage grows. The footer also shows the current model and permission mode.
  • Pinned todo panel — subscribes to TodoWriteTool's module store, so the live todo list stays pinned above the input box (not covered by transcript). Auto-collapses when all items are completed.
  • Stop — Esc or Ctrl+C while generating stops the current LLM output (engine.interrupt(), shows (stopped)) without exiting; exitOnCtrlC: false so Ink doesn't preempt our handler. Idle Ctrl+C starts a 1.5s double-press exit timer.
  • Non-blocking input — you can keep typing while the agent runs. Plain input is queued and injected at the top of the agent's next turn (the current model call / tool execution is not interrupted); /stop interrupts immediately.
  • Cursor editing — ←/→ move the cursor within the input, backspace deletes at the cursor, characters insert at the cursor. IME (e.g. pinyin) multi-codepoint words advance the cursor by the number of code points, so it lands at the end (你好▋, not 你▋好).
  • Plan approval — bordered yellow box with [y]/[n].
  • Permission approval — bordered magenta box [y]/[n] for write tools in default mode.
  • Model selector/model (no arg) opens an arrow-navigable list (cyan cursor, Enter confirms, Esc cancels).
  • History selector/history opens an arrow-navigable list of past sessions for the cwd; Enter resumes one (re-hydrates the transcript + context).
  • Bare exit/quit also exits.

REPL 界面

src/ink/App.tsx 是 Ink/React TUI。值得注意的行为:

  • 启动 banner——4 行半块 ASCII 艺术字 harness code,通过 <Static> 渲染,持久化在滚动缓冲里,/resume 重挂载也不丢失。
  • <Static> 转录——对话历史(和 banner)通过 Ink 的 <Static> 组件渲染,每条只写一次进终端滚动缓冲、不再重绘。Ink 的 logUpdate 只重绘底部固定区(cwd 提示、spinner、输入框、footer、面板),所以终端能自由滚动——不会被吸顶或吸底。
  • 转录颜色——用户绿色、模型青色、工具调用 dim 灰;每条工具行显示目标(文件路径/命令/todo 计数),不只是工具名。
  • Spinner——braille ⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏(80ms)带活动标签(thinking / running BashTool / writing)。
  • 上下文进度条——src/ink/barGlyph.ts 渲染 [███████▓▒░░░░░░░] ctx: 12.3k/400k (3%) · gpt-5.5 · default · $0.0028,带流动的 高光和巡游的 暗点(即使 0% 也一直在动);颜色随占用率青→黄→红渐变。底栏还显示当前模型和权限模式。
  • 固定 todo 面板——订阅 TodoWriteTool 的模块存储,实时 todo 列表钉在输入框上方(不被转录覆盖)。全部完成时自动收起。
  • 停止——生成中按 Esc 或 Ctrl+C 停止当前 LLM 输出(engine.interrupt(),显示 (stopped))但不退出;exitOnCtrlC: false 让 Ink 不抢先处理。空闲时 Ctrl+C 启动 1.5s 双击退出计时。
  • 非阻塞输入——agent 运行时能继续打字。普通输入排队,在 agent 下一轮顶部注入(不打断当前模型调用/工具执行);/stop 立即中断。
  • 光标编辑——←/→ 在输入框内移动光标,backspace 在光标处删除,字符在光标处插入。IME(如拼音)多 code point 整词按字符数推进光标,停到末尾(你好▋,不是 你▋好)。
  • Plan 审批——黄色边框框 [y]/[n]
  • 权限审批——default 模式下写工具的品红色边框框 [y]/[n]
  • 模型选择器——/model(无参)打开箭头可导航列表(青色 光标,Enter 确认,Esc 取消)。
  • 历史选择器——/history 打开当前 cwd 历史会话的箭头可导航列表;Enter resume 一个(水合转录 + 上下文)。
  • exit/quit 也退出。

Token usage & cost tracking

src/services/api/usage.ts (UsageTracker) accumulates per-model input/output/cacheRead/cacheWrite tokens and a USD total. Each model call's usage is reported via the onUsage callback in query.tsQueryEngine.submitMessageusageTracker.add, plus a UI callback so the REPL footer refreshes live.

Pricing (DEFAULT_PRICING, USD per 1M tokens) is a placeholder estimate — the proxy does not bill. Override per model in the table. Unknown models fall back to gpt-5.5 pricing. formatCost shows $X.XXXX under $0.50, else $X.XX.

  • REPL footer: ctx: X/400k (Y%) · Total: $Z (live).
  • /cost: per-model breakdown (in / out / cache-read / cache-write — $cost) + total.
  • engine.getContextTokens() estimates the current conversation's token count (chars/4).

Token 用量与成本追踪

src/services/api/usage.tsUsageTracker)按模型累积 input/output/cacheRead/cacheWrite token 和 USD 总额。每次模型调用的 usagequery.tsonUsage 回调 → QueryEngine.submitMessageusageTracker.add,同时触发 UI 回调让 REPL 底栏实时刷新。

定价(DEFAULT_PRICING,USD/百万 token)是占位估算——代理不计费。可在表中按模型覆盖。未知模型 fallback 到 gpt-5.5 定价。formatCost 在 $0.50 以下显示 $X.XXXX,否则 $X.XX

  • REPL 底栏:ctx: X/400k (Y%) · Total: $Z(实时)。
  • /cost:按模型的明细(in / out / cache-read / cache-write — $cost)+ 总额。
  • engine.getContextTokens() 估算当前对话 token 数(chars/4)。

Slash commands

20 built-in commands (src/commands.ts). The REPL routes /name args through parseSlashCommand; commands return a synthetic assistant message, an injected prompt, or an action. While the agent is running, /stop interrupts immediately and plain input is queued (see The REPL UI).

Command Action
/help List commands
/clear Clear the conversation
/compact Compact context now (engine.compactNow)
/model Interactive selector (arrow keys + Enter); /model <id> switches directly
/models List the catalog
/config Show effective config (apiKey redacted)
/cost Token + cost breakdown
/skills List available skills
/memory Show loaded memories; /memory save extracts new ones
/init Generate a CLAUDE.md for the project
/hooks List configured hooks
/sessions List resumable sessions for the cwd
/resume <id> Resume a session
/history Browse and resume past sessions (arrow-key selector)
/export Export the transcript to markdown
/plan Toggle plan mode
/bypass Toggle bypass mode (auto-confirm all tool use); /bypass off restores default, /bypass auto for classifier mode
/stop Force-stop the running agent loop (interrupts tools/thinking)
/new Start a fresh conversation (clears history)
/exit Exit

斜杠命令

20 个内置命令(src/commands.ts)。REPL 把 /name argsparseSlashCommand 路由;命令返回合成 assistant 消息、注入 prompt 或 action。agent 运行时,/stop 立即中断,普通输入排队(见 REPL 界面)。

命令 作用
/help 列出命令
/clear 清空对话
/compact 立即压缩上下文(engine.compactNow
/model 交互选择器(箭头 + Enter);/model <id> 直接切换
/models 列出目录
/config 显示生效配置(apiKey 脱敏)
/cost Token + 成本明细
/skills 列出可用 skill
/memory 显示已加载记忆;/memory save 提取新记忆
/init 为项目生成 CLAUDE.md
/hooks 列出已配置 hook
/sessions 列出 cwd 可 resume 的会话
/resume <id> resume 会话
/history 浏览并 resume 历史会话(箭头选择器)
/export 导出转录为 markdown
/plan 切换 plan 模式
/bypass 切换 bypass 模式(自动确认所有工具);/bypass off 恢复默认,/bypass auto 用分类器模式
/stop 强制停止运行中的 agent loop(截断工具/思考)
/new 开启新对话(清空历史)
/exit 退出

Testing

npm test            # unit + filesystem + integration (mock API), ~1.3s
npm run test:api    # + real-API integration tests (RUN_API_TESTS=1)

204 tests (199 pass, 5 skipped without RUN_API_TESTS=1):

  • tests/unit/ — bash parser/safety, permission pipeline, config priority chain, hooks (function + command), session store, memory extraction, plan mode, auto classifier, compaction, usage tracking, bar glyph, UI rendering + cursor/IME interaction (via ink-testing-library).
  • tests/fs/ — file edit/read, glob/grep, bash.
  • tests/api/ — streaming, tool-use (real API).
  • tests/mcp/ — MCP stdio with a fixture server.
  • tests/integration/loop/ — end-to-end agent loop (real API).

Test credentials live in tests/setup.ts; API tests are gated behind RUN_API_TESTS=1.


测试

npm test            # unit + 文件系统 + 集成(mock API),约 1.3s
npm run test:api    # + 真实 API 集成测试(RUN_API_TESTS=1)

204 个测试(199 pass,5 个无 RUN_API_TESTS=1 时 skip):

  • tests/unit/ —— bash 解析/安全、权限流水线、配置优先级链、hooks(function + command)、会话存储、记忆提取、plan 模式、auto 分类器、压缩、usage 追踪、bar glyph、UI 渲染 + 光标/IME 交互(用 ink-testing-library)。
  • tests/fs/ —— 文件 edit/read、glob/grep、bash。
  • tests/api/ —— 流式、tool-use(真实 API)。
  • tests/mcp/ —— 带 fixture server 的 MCP stdio。
  • tests/integration/loop/ —— 端到端 agent 循环(真实 API)。

测试凭证在 tests/setup.ts;API 测试由 RUN_API_TESTS=1 门控。


Build

tsup (tsup.config.ts) produces a single ESM bundle at dist/main.js (~205 KB). All dependencies are external (resolved from node_modules at runtime); only src/ is bundled. react-devtools-core (a conditional Ink import) is aliased to a no-op shim in src/shims/react-devtools-core.ts. A #!/usr/bin/env node banner makes the bundle directly executable.


构建

tsuptsup.config.ts)生成单个 ESM bundle dist/main.js(约 205 KB)。所有依赖 external(运行时从 node_modules 解析);只 bundle src/react-devtools-core(Ink 的条件导入)被 alias 到 no-op shim(src/shims/react-devtools-core.ts)。#!/usr/bin/env node banner 让 bundle 可直接执行。


Status

Core usable. Single-agent ReAct loop, 13 tools, permission pipeline + Bash AST safety, CLAUDE.md/auto-memory, compaction (400K window), session persistence + resume, hooks (command + function), plan mode, auto classifier, MCP stdio, sub-agent, skills, interactive Ink REPL with spinner/progress bar/pinned todo panel/permission prompts/model+history selectors/non-blocking input/free terminal scrolling, cursor editing (incl. IME), token + cost tracking.

Deferred per scope decision: Bridge remote control, Vim mode, ScrollBox/Yoga/mouse UI, Swarm/Coordinator multi-agent teams, enterprise MDM/policy, telemetry, plugin marketplace, MCP SSE/HTTP/WS transports, WebSearch backend, OAuth.

Implemented against the reverse-engineered docs in docs/ (Claude Code 2.1.88 sourcemap).

状态

核心可用。 单 agent ReAct 循环、13 个工具、权限流水线 + Bash AST 安全、CLAUDE.md/auto-memory、压缩(400K 窗口)、会话持久化 + resume、hooks(command + function)、plan 模式、auto 分类器、MCP stdio、子 agent、skill、带 spinner/进度条/固定 todo 面板/权限提示/模型+历史选择器/非阻塞输入/终端自由滚动/光标编辑(含 IME)的交互式 Ink REPL、token + 成本追踪。

按范围决策推迟:Bridge 远程控制、Vim 模式、ScrollBox/Yoga/鼠标 UI、Swarm/Coordinator 多 agent、企业 MDM/策略、遥测、插件市场、MCP SSE/HTTP/WS 传输、WebSearch 后端、OAuth。

基于 docs/(Claude Code 2.1.88 sourcemap)逆向还原的文档实现。


About

harness-code,一个具备harness约束的code Agent,供个人学习Agent使用

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages