Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
95 commits
Select commit Hold shift + click to select a range
af68874
fix(engine): close two goal-supervision longevity gaps (#187)
andybons Aug 26, 2026
b855740
fix(engine): revive a Reap()-ed descendant for task verbs (#186)
andybons Aug 26, 2026
bc4429f
feat(engine): emit per-turn latency and cache metrics to stdout (#188)
andybons Aug 26, 2026
28dc486
feat(anthropic): emit defer_loading and the bm25 tool search tool (#189)
andybons Aug 26, 2026
cdb09ac
test(plugin): prove concurrent RPCs to one plugin are id-multiplexed …
andybons Aug 26, 2026
02742f8
fix(engine): refuse write_file overwrite of an unread existing file (…
andybons Aug 26, 2026
60783fd
fix(engine): auto-continue a turn cut off by max_tokens instead of pa…
andybons Aug 26, 2026
0ea2dae
feat(engine): run one assistant message's tool calls concurrently (#194)
andybons Aug 26, 2026
3b6e8cc
provider/openai,config: allow a second native Responses provider (#197)
andybons Aug 27, 2026
fd10d27
feat(engine,server): answer session reads from a metadata index (#195)
andybons Aug 27, 2026
0fe0076
feat(engine): tell the model to batch independent tool calls (#200)
andybons Aug 27, 2026
0573909
feat(server,cmd): surface slow handlers and long GC pauses (#201)
andybons Aug 27, 2026
caa1f82
fix(engine): judge a page's final record by the fold's own rule (#198)
andybons Aug 27, 2026
77e5aec
fix(engine): bound the memory a concurrent read batch holds (#203)
andybons Aug 27, 2026
a2cc1f4
test(engine): adversarial suite for the concurrent tool executor (#202)
andybons Aug 27, 2026
eb085ea
fix(engine): announce an AGENTS.md truncation to the model and the lo…
andybons Aug 27, 2026
6c27d31
feat(engine): split an oversize AGENTS.md into a head and an outline …
andybons Aug 27, 2026
1d8fd5f
fix(engine): disambiguate folded page records (#207)
andybons Aug 27, 2026
795289b
test: verify child lineage.status on GET /session list (#208)
andybons Aug 27, 2026
4f9ee65
engine: journal snapshotting — bound LoadSession replay cost (#209)
andybons Aug 27, 2026
69a6b8b
server: stop cold-replaying sessions on every list; engine: refuse a …
andybons Aug 28, 2026
c438716
server: document task_depth in the journal record schema (#211)
andybons Aug 28, 2026
276923e
docs(agents): split repository guidance by scope (#212)
andybons Aug 28, 2026
aeead5c
provider/openai,config: allow a provider to omit Responses request pa…
andybons Aug 29, 2026
048cbe2
engine,provider: delegate a turn to the Claude Code CLI over stream-j…
andybons Aug 30, 2026
960309d
provider/openai,config: sanitize tool schemas for the Responses tool-…
andybons Aug 30, 2026
fbc3712
provider/openai: stream the Codex Responses API over websockets (#216)
andybons Aug 30, 2026
1c907ed
engine: forward MCP, effort, reasoning, subagent nesting, and metrics…
andybons Aug 31, 2026
e5b72a5
Capture subscription rate-limit usage on the session (#218)
andybons Aug 31, 2026
a4da2ca
fix(engine): pass --forward-subagent-text to the claude-code CLI (#219)
andybons Aug 31, 2026
64b0ff6
engine: return on the claude-code result event, not stdout EOF (#220)
andybons Aug 31, 2026
e6a0487
feat(config): add append_system_prompt (#221)
andybons Sep 1, 2026
296f99b
feat(engine,server): harness-hosted MCP server for the claude-code la…
andybons Sep 1, 2026
129749b
feat(server,engine): expose task and list-only model over the MCP shi…
andybons Sep 1, 2026
ae29764
fix(server): race-close the transcript bootstrap resume point (#224)
andybons Sep 1, 2026
62705f4
feat(engine): disallow native subagent spawning in claude-code lane (…
andybons Sep 1, 2026
21d18ea
Revert "feat(engine): disallow native subagent spawning in claude-cod…
andybons Sep 1, 2026
b6bd8a8
fix(engine): deliver and commit task notifications on claude-code lan…
andybons Sep 1, 2026
4e632ec
Reapply "feat(engine): disallow native subagent spawning in claude-co…
andybons Sep 1, 2026
c6cf78a
feat(engine,provider,server): add per-session Codex service tier (#229)
andybons Sep 1, 2026
a01deac
feat(server): accept a client-supplied user-message id (#230)
andybons Sep 1, 2026
7918b6d
fix(engine): keep claude-code stdin open for mid-turn queue delivery …
andybons Sep 1, 2026
9e0a1e8
fix(engine): stop the claude-code pump losing writes and wedging on s…
andybons Sep 1, 2026
dd5afee
fix(engine): store a reasoning turn as one assistant message with bot…
andybons Sep 2, 2026
5f8975a
feat(engine): surface claude-code session cost via subscription_usage…
andybons Sep 2, 2026
9201177
feat(modelmeta): give codex a context-window entry like claude-code (…
andybons Sep 2, 2026
977fe45
feat(server,engine): accept image and PDF attachments on prompt_async…
andybons Sep 2, 2026
f0c257e
fix(engine): ask the claude-code CLI for summarized thinking (#237)
andybons Sep 2, 2026
4e9e9c1
feat(server,engine): accept blob attachments on the durable enqueue p…
andybons Sep 2, 2026
42f63d6
fix(engine): emit a turn segment's deltas before its own message (#239)
andybons Sep 2, 2026
0495e61
fix(engine): assemble one upstream response into one message (#240)
andybons Sep 2, 2026
e21f415
feat(engine): add compaction.started event for live progress (#241)
andybons Sep 2, 2026
f87a6c6
fix(engine): preserve claude-code session state across snapshot loads…
andybons Sep 2, 2026
f633386
docs(agents): scope mandatory TDD and set the comment default (#244)
andybons Sep 2, 2026
af098c0
chore(monitor): remove the embedded session monitor (#246)
andybons Sep 2, 2026
fc7e1fa
feat(provider): chain codex websocket responses (#247)
andybons Sep 3, 2026
0af1b07
feat(provider): compress codex HTTP requests with zstd (#248)
andybons Sep 3, 2026
bbb26e0
fix(provider): recover a codex chain miss onto a fresh connection (#249)
andybons Sep 3, 2026
e410d66
fix(server): release a compact turn's run slot even if Compact panics…
andybons Sep 3, 2026
6a39605
feat(provider): surface reasoning for codex and gemini (#250)
andybons Sep 3, 2026
8735fdf
fix(engine): disallow claude code's native loop and cron tools (#252)
andybons Sep 3, 2026
bb16938
feat: unify session.send and prompt_async for a managed child (#253)
andybons Sep 3, 2026
7313589
feat(server): add live_from event-journal tip to the transcript boots…
andybons Sep 3, 2026
8d28f24
feat(server): add per-message seqs to the transcript tail envelope (#…
andybons Sep 4, 2026
1af8790
fix(cmd/harness): invalidate plugin cache on script content change (#…
andybons Sep 5, 2026
cf1c041
chore(comments): simplify Go documentation (#257)
andybons Sep 5, 2026
3b5dc91
feat(engine,server): journal primitives for an external consumer (#258)
andybons Sep 7, 2026
5c149d2
fix(engine): compact a delegated session's journal before a native mo…
andybons Sep 8, 2026
a3251ca
fix(engine): stamp operator-batch prompts with origin and provenance …
andybons Sep 8, 2026
f613b0e
feat(engine): tighten the base system prompt + fix AGENTS.md preceden…
andybons Sep 8, 2026
73e04b4
feat(server): forward the durable journal to an event sink (#262)
andybons Sep 8, 2026
e915cd2
feat(engine): label subscription vs API billing on the model tool (#263)
andybons Sep 8, 2026
28ad003
fix(engine): surface and unwedge a root session's own interrupted tur…
andybons Sep 9, 2026
92a69e4
feat(server): serve a windowed stream_from+limit transcript bootstrap…
andybons Sep 9, 2026
155ede5
fix(engine): keep ambient status chain-stable and name every chain re…
andybons Sep 9, 2026
c110efd
fix(provider): report a chain prefix refusal item as a numeric field …
andybons Sep 9, 2026
9fa997a
test(engine): block on the child end observer instead of on status (#…
andybons Sep 9, 2026
9fc20a5
test(e2e): a real harness binary reports last_turn on a failed turn (…
andybons Sep 9, 2026
0045923
feat(engine): report service tier and effort on turn_metrics (#271)
andybons Sep 9, 2026
286db4f
test(engine): settle the root's resume before simulating the crash (#…
andybons Sep 9, 2026
380d5cd
fix(openai): recover a codeless invalid previous_response_id chain mi…
andybons Sep 10, 2026
c3959ab
fix(engine): pin ambient status append-only so Codex chains hold (#276)
andybons Sep 10, 2026
102ee2c
feat(server): filter durable event sink records (#277)
andybons Sep 10, 2026
bd862da
feat(server): timestamp durable event sink records (#278)
andybons Sep 10, 2026
92136d1
fix(server): stop retrying permanent event sink rejections (#279)
andybons Sep 10, 2026
24010c3
feat(prompt): make concise user-visible output the adaptive default (…
andybons Sep 10, 2026
14d6030
feat(prompt): stop the model narrating change history in code (#281)
andybons Sep 10, 2026
25f41b9
fix(modelmeta): add gpt-6-astra's context window (#286)
andybons Sep 12, 2026
0a7a7d9
fix(engine): drain a terminal subagent's orphaned prompt queue (#287)
andybons Sep 12, 2026
5e9c32c
chore: add mit license (#288)
andybons Sep 13, 2026
915a953
ci: move fuzz workflow actions to node24 runtimes (#289)
andybons Sep 13, 2026
ac3a963
fix(engine): make the turn_metrics retry field zero-based (#291)
andybons Sep 15, 2026
2aa9e6d
docs: trim comments (#292)
andybons Sep 16, 2026
18f8f20
test(engine): deflake TestFinalizeTurnDrainsOrphanedQueueOnExplicitCa…
andybons Sep 16, 2026
5daca23
feat(engine): hand MCP deferral to anthropic's server-side tool search
andybons Aug 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
27 changes: 13 additions & 14 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,20 +23,27 @@ jobs:
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
# Production code is stdlib-only, enforced by the absence of
# non-test imports (`go list -deps ./cmd/...` must never contain a
# third-party package). Test-only dependencies are allowed;
# currently just pgregory.net/rapid, which is why go.sum exists.
# Production dependencies must remain pure Go. Module caching covers
# both production adapters and test-only support packages.
cache: true

- name: Check formatting
run: |
unformatted=$(gofmt -l .)
if [ -n "$unformatted" ]; then
printf 'gofmt required for:\n%s\n' "$unformatted"
for file in $unformatted; do
gofmt -d "$file"
done
exit 1
fi

- run: go build ./...

- run: go test -race ./...

- run: go vet ./...

- run: test -z "$(gofmt -l .)"

- name: Inspector unit tests
# The session inspector is a single build-free HTML file; its pure
# helpers (SSE parser + formatters) are unit-tested with node's built-in
Expand All @@ -47,14 +54,6 @@ jobs:
# test file, which the runner then runs directly.
run: node --test tools/inspector/*_test.mjs

- name: Monitor unit tests
# tools/monitor is the same pattern as the inspector above: a single
# build-free HTML file whose pure helpers (SSE parser, activity
# reducer, transcript fold, route codec, formatters) are extracted
# from a TESTABLE region and unit-tested with node's built-in test
# runner. Same glob-not-directory rationale as the inspector step.
run: node --test tools/monitor/*_test.mjs

- name: Hub unit tests
# tools/hub's pure logic has been unit-tested by hub_test.mjs since
# its introduction (AGENTS.md's "Development hub" section documents
Expand Down
10 changes: 6 additions & 4 deletions .github/workflows/claude-code-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,16 +39,18 @@ jobs:
Use the /code-review skill to review this pull request.

This is a Go agent harness (engine, provider transcoders, plugin
protocol). Read AGENTS.md first and review with these priorities:
protocol). Read the root AGENTS.md first. Then read every scoped
AGENTS.md that governs a changed path. Review with these priorities:
- Race conditions, deadlocks, missing synchronization — the engine,
plugin host, and streams are heavily concurrent
- Protocol correctness (plugin JSON-RPC framing, hook chaining
semantics, manifest/lazy-spawn invariants)
- Transcoding correctness (canonical <-> provider wire formats,
ProviderData replay/drop rules, tool-call ID stability)
- Startup-speed regressions: any init-time network, disk, or
subprocess work is a bug per AGENTS.md
- Testing rules from AGENTS.md: TDD, no raw sleeps, synctest for
- Startup-speed regressions: init-time network or subprocess work,
and disk reads beyond the permitted user/project config path,
are bugs per the root and cmd/harness scoped AGENTS.md files
- Testing rules from the root AGENTS.md: TDD, no raw sleeps, synctest for
timer logic
- Bugs, logic errors, nil/zero-value pitfalls, unchecked errors

Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/fuzz.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ jobs:
cache: true

- name: Restore fuzz corpus cache
uses: actions/cache@v4
uses: actions/cache@v6
with:
# Go's fuzzing engine persists interesting inputs it discovers
# under GOCACHE/fuzz, separate from the committed seed corpus in
Expand All @@ -90,7 +90,7 @@ jobs:

- name: Upload failing corpus entries
if: failure()
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
# A fuzzing failure writes the minimized failing input under
# testdata/fuzz/<Target>/ in the repo tree (this is distinct from
Expand Down
2,310 changes: 198 additions & 2,112 deletions AGENTS.md

Large diffs are not rendered by default.

21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2026 Andy Bonventre

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
38 changes: 35 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ A fast, extensible, composable agent harness in Go.
- **Composable** — headless engine, event streams, client/server, MCP both directions
- **Model-fluid** — swap providers/models mid-session or per-subagent with no migration

See [AGENTS.md](AGENTS.md) for architecture and design decisions.
See [AGENTS.md](AGENTS.md) for repository-wide rules and the scoped
instruction index. Each major subsystem has its own concise `AGENTS.md`.
See [docs/README.md](docs/README.md) for technical documentation.

## Configuration

Expand Down Expand Up @@ -56,5 +58,35 @@ and `api_key_env` above, so `"model": "openrouter/anthropic/claude-sonnet-5"`
works as soon as `OPENROUTER_API_KEY` is set. Any `openrouter` entry in
config — even a partial one — overrides the built-in default entirely.

An unrecognized `type`, or an `openai-compat` entry missing `base_url`, fails
config loading loudly rather than silently registering nothing.
An endpoint that speaks the OpenAI **Responses** API rather than
chat-completions uses `type: "openai"`, which builds the same native adapter
the built-in `openai` family uses. It works under any map key, so a second
Responses endpoint can sit beside the built-in one, and `responses_path`
points it at an endpoint that does not serve `/v1/responses`:

```json
{
"providers": {
"vendor": {
"type": "openai",
"base_url": "https://api.vendor.example",
"api_key_env": "VENDOR_API_KEY",
"responses_path": "/backend/responses"
}
}
}
```

`"model": "vendor/some-model"` then routes there, passing `some-model`
through as the model id. `responses_path` defaults to `/v1/responses` and is
also accepted on the built-in `openai` entry; it is rejected on any other
kind of entry, since no other adapter reads it.

An unrecognized `type`, an `openai-compat` or `openai` entry missing
`base_url`, or a `responses_path` on an entry that builds neither Responses
adapter, fails config loading loudly rather than silently registering
nothing.

## License

Harness is licensed under the [MIT License](LICENSE).
73 changes: 73 additions & 0 deletions cmd/harness/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Harness command instructions

These rules apply to `cmd/harness/`. Harness does not merge ancestor files.
If root guidance is not active, locate the Git root and read
`<repo-root>/AGENTS.md`. Resolve repository paths from that root.

## Composition root

Keep the command package thin. It resolves config, constructs providers and
managers, and composes the engine, server, and embedded local tools.

Do not move engine behavior into command handlers. Do not make `server` import
`tools/*`; inject pages and dependencies through options.

## Startup budget

Keep `harness --version` on the millisecond startup path.

- Do not add network access at startup.
- Before first output, limit disk reads to the user and project config files.
- Do not start a long-lived plugin process before its first hook or tool call.
A bounded manifest probe can run for a missing or stale cache entry.
- Do not start MCP servers or provider clients before first use.
- Do not scan skills or project instructions at `NewSession`.
- Do not add `init()` side effects.
- Keep plugin manifests and model metadata local and static on the hot path.
- Keep production dependencies pure Go.

Run the startup budget tests after a change to command initialization.

## Config and environment resolution

The command layer resolves environment variables. The engine must not read
operator environment variables directly.

Keep one decision point for each config precedence rule. Preserve explicit
zero, negative opt-out, and unset distinctions.

Validate adapter-only fields against the adapter that an entry builds, not the
provider map key alone. Fail loudly on an unreadable or unknown value.

Keep run and serve wiring in parity for shared engine settings.

## Provider construction

A provider-map key is the model-reference family. Pass that family into native
Responses clients so opaque data cannot cross endpoints.

Do not validate credentials during registry construction. The first provider
request owns credential validation.

## Hub composition

`cmd/harness` may import `tools/hub`. The server may
not.

Only allow empty-token unauthenticated service when `resolveUnauthenticated`
proves loopback or receives the explicit non-loopback opt-in. Keep the two
warning messages distinct.

## GC and pprof diagnostics

Use `runtime/metrics` for GC pause observation. Do not use
`runtime.ReadMemStats` in the watcher.

Never import `net/http/pprof`. Keep the command-level default-mux regression
test because the binary import graph is wider than the server package.

## Tests

Test flag and environment precedence as tables. Cover malformed values and
explicit zero values. Do not use a live provider or a real deployment command
in ordinary command tests.
1 change: 1 addition & 0 deletions cmd/harness/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@AGENTS.md
29 changes: 29 additions & 0 deletions cmd/harness/append_system_prompt_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package main

import (
"reflect"
"testing"

"github.com/majorcontext/harness/config"
)

func TestAppendSystemSegments(t *testing.T) {
tests := []struct {
name string
cfg *config.Config
flag string
want []string
}{
{"unset", nil, "", nil},
{"config", &config.Config{AppendSystemPrompt: []string{"one", "two"}}, "", []string{"one", "two"}},
{"flag", nil, "flag", []string{"flag"}},
{"both", &config.Config{AppendSystemPrompt: []string{"one", "two"}}, "flag", []string{"one", "two", "flag"}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := appendSystemSegments(tt.cfg, tt.flag); !reflect.DeepEqual(got, tt.want) {
t.Errorf("appendSystemSegments = %v, want %v", got, tt.want)
}
})
}
}
3 changes: 1 addition & 2 deletions cmd/harness/cache_ttl_parity_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@ import (
// a provider package — but until now nothing but a comment kept them in step.
// config.validateCacheTTL accepts against one copy and anthropic.
// resolveCacheTTL accepts against the other, so a value added to one list
// alone would be accepted at load and then rejected at the first Stream call,
// or vice versa: the silent-drift class this PR set out to remove.
// alone would be accepted at load and then rejected at the first Stream call.
//
// cmd/harness is the right home for the check because it is the one package
// that already imports both, and it is the seam that carries a configured
Expand Down
84 changes: 84 additions & 0 deletions cmd/harness/claude_code_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package main

import (
"testing"

"github.com/majorcontext/harness/config"
"github.com/majorcontext/harness/engine"
"github.com/majorcontext/harness/message"
"github.com/majorcontext/harness/provider/claudecode"
)

// TestRegistryClaudeCodeCLIRegistersStubUnderKey proves a claude-code-cli
// entry registers a claudecode.Client under its providers-map key, exactly
// like registerOpenAICompatProviders does for its own type — this is what
// makes Session.ModelSupported accept a swap to a claude-code model ref
// (see registerClaudeCodeProviders' own doc comment).
func TestRegistryClaudeCodeCLIRegistersStubUnderKey(t *testing.T) {
reg := registry(&config.Config{Providers: map[string]config.Provider{
"claude-code": {Type: config.TypeClaudeCodeCLI, BinaryPath: "/usr/local/bin/claude"},
}})
if _, ok := reg["claude-code"].(claudecode.Client); !ok {
t.Fatalf("claude-code provider is %T, want claudecode.Client", reg["claude-code"])
}
ref, err := message.ParseModelRef("claude-code/sonnet")
if err != nil {
t.Fatalf("ParseModelRef: %v", err)
}
if _, err := reg.For(ref); err != nil {
t.Errorf("reg.For(%s): %v, want a registered adapter", ref, err)
}
}

// TestClaudeCodeConfigForTranslatesProviderFields proves
// claudeCodeConfigFor carries BinaryPath/ExtraArgs/PermissionMode from a
// config.Provider entry into engine.ClaudeCodeConfig — the one translation
// point between the file-config Provider type and the engine's own
// backend-agnostic Config (engine deliberately does not import config; see
// claudeCodeConfigFor's own doc comment).
func TestClaudeCodeConfigForTranslatesProviderFields(t *testing.T) {
cfg := &config.Config{Providers: map[string]config.Provider{
"claude-code": {
Type: config.TypeClaudeCodeCLI,
BinaryPath: "/opt/claude/bin/claude",
ExtraArgs: []string{"--mcp-config", "/tmp/mcp.json"},
PermissionMode: "acceptEdits",
},
}}
got := claudeCodeConfigFor(cfg, claudecode.Family)
want := engine.ClaudeCodeConfig{
BinaryPath: "/opt/claude/bin/claude",
ExtraArgs: []string{"--mcp-config", "/tmp/mcp.json"},
PermissionMode: "acceptEdits",
}
if got.BinaryPath != want.BinaryPath || got.PermissionMode != want.PermissionMode {
t.Errorf("claudeCodeConfigFor = %+v, want %+v", got, want)
}
if len(got.ExtraArgs) != len(want.ExtraArgs) {
t.Fatalf("ExtraArgs = %+v, want %+v", got.ExtraArgs, want.ExtraArgs)
}
for i := range want.ExtraArgs {
if got.ExtraArgs[i] != want.ExtraArgs[i] {
t.Errorf("ExtraArgs[%d] = %q, want %q", i, got.ExtraArgs[i], want.ExtraArgs[i])
}
}
}

// TestClaudeCodeConfigForAbsentEntryYieldsZeroValue proves a config with no
// matching entry (or a nil *config.Config, the same defensive shape every
// other cmd/harness helper here handles) yields the zero ClaudeCodeConfig
// rather than panicking — engine.newSession's own BinaryPath default
// ("claude") then applies.
func TestClaudeCodeConfigForAbsentEntryYieldsZeroValue(t *testing.T) {
assertZero := func(t *testing.T, got engine.ClaudeCodeConfig) {
t.Helper()
if got.BinaryPath != "" || got.PermissionMode != "" || len(got.ExtraArgs) != 0 {
t.Errorf("claudeCodeConfigFor = %+v, want the zero value", got)
}
}
assertZero(t, claudeCodeConfigFor(nil, claudecode.Family))
cfg := &config.Config{Providers: map[string]config.Provider{
"anthropic": {APIKeyEnv: "X"},
}}
assertZero(t, claudeCodeConfigFor(cfg, claudecode.Family))
}
13 changes: 4 additions & 9 deletions cmd/harness/create_phase_logger_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ import (
"time"
)

// TestCreatePhaseLoggerEmptiesMapOnTotal is a regression test for the
// phase-accumulator leak (PR #87 review): a create that reports every
// TestCreatePhaseLoggerEmptiesMapOnTotal verifies that a create that reports every
// intermediate phase before "total" must leave byID empty afterward, not
// just render a sensible summary line.
func TestCreatePhaseLoggerEmptiesMapOnTotal(t *testing.T) {
Expand Down Expand Up @@ -42,13 +41,9 @@ func TestCreatePhaseLoggerEmptiesMapOnTotal(t *testing.T) {
}
}

// TestCreatePhaseLoggerHandlesTotalWithoutIntermediatePhases is the
// regression case the leak actually manifested as: a saturated storage
// volume makes Persist fail on every create, so handleCreate's defer (see
// server/handlers.go) reports "new_session" then jumps straight to "total"
// with no persist/register/emit_created in between. The summary line must
// still render — with the missing phases simply absent — and the map entry
// must still be reclaimed, not orphaned.
// TestCreatePhaseLoggerHandlesTotalWithoutIntermediatePhases verifies that a
// failed create can report "total" directly. The summary omits missing phases
// and removes the map entry.
func TestCreatePhaseLoggerHandlesTotalWithoutIntermediatePhases(t *testing.T) {
var buf bytes.Buffer
logger := slog.New(slog.NewTextHandler(&buf, nil))
Expand Down
Loading