Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
87 commits
Select commit Hold shift + click to select a range
843131f
feat(execprofile): add the zeromaxing posture rung
gnanam1990 Jul 28, 2026
6d6b33c
feat(agent): inject the zeromaxing reminders below the cache breakpoint
gnanam1990 Jul 28, 2026
455e770
feat(tui): make /effort zeromaxing the entry point on both selection …
gnanam1990 Jul 28, 2026
e885d79
fix(execprofile): describe the posture's delta against the caller's o…
gnanam1990 Jul 28, 2026
ac82364
fix(tui): apply the posture's effort fill on models the catalog canno…
gnanam1990 Jul 28, 2026
b667521
fix(tui): treat an unlisted model as unknown, not as having no reason…
gnanam1990 Jul 28, 2026
91218af
feat(specialist): capture and sequentially execute a declared plan
gnanam1990 Jul 28, 2026
940aba2
feat(cli): register the orchestrate tool and map a partial plan to ex…
gnanam1990 Jul 28, 2026
3b1df93
feat(tui): flip the orchestrate posture gate from the real handlers
gnanam1990 Jul 28, 2026
e682cb6
fix(specialist): make the plan tool's parent grant real and empty mea…
gnanam1990 Jul 29, 2026
7274af3
fix(tui): make both effort doors ask the same question
gnanam1990 Jul 29, 2026
e170c6f
fix(cli): record the failed plan task's session and spend
gnanam1990 Jul 29, 2026
3ee471d
fix(cli): measure the run's real depth for plan admission
gnanam1990 Jul 29, 2026
9c17a16
test(specialist): compare the two grant enforcement points against ea…
gnanam1990 Jul 29, 2026
8199c43
feat(tui): make a running plan visible
gnanam1990 Jul 29, 2026
06c4a06
feat(tui): add the /plans panel with the plan's dependency shape
gnanam1990 Jul 29, 2026
86db276
fix(specialist): a plan task inherits the parent's model
gnanam1990 Jul 29, 2026
126ed00
fix(specialist): a resumed specialist keeps the run's model
gnanam1990 Jul 29, 2026
96439f8
fix(specialist): the two read-only tool sets agree again
gnanam1990 Jul 29, 2026
a7f5445
perf(agent): drop the confirmation policy from a run that cannot act
gnanam1990 Jul 29, 2026
13d02de
feat(specialist,tui): unbounded plans by default, collapsed plan panel
gnanam1990 Jul 29, 2026
b904c3f
feat(tui): fade finished plan tasks, and say what /effort accepts
gnanam1990 Jul 29, 2026
3189641
fix(tui): the effort picker offers what /effort actually accepts
gnanam1990 Jul 29, 2026
e59394c
fix(tui): five reporting defects a real plan run exposed
gnanam1990 Jul 29, 2026
5620403
fix(tui): the plan budget line counts while the plan runs
gnanam1990 Jul 29, 2026
f9ecde0
feat(tui): the plan detail view — phases left, live agent detail right
gnanam1990 Jul 29, 2026
58b3b7c
feat(tui): the running plan fills the sidebar's PLAN section, in colour
gnanam1990 Jul 29, 2026
2957bd6
feat(tui): the plan owns the right column — progress bar, task list, …
gnanam1990 Jul 29, 2026
3308603
feat(tui): the zeromaxing posture glows, in the footer and where it i…
gnanam1990 Jul 29, 2026
9909f7e
feat(tui): hover on the posture chip and the plan rows, and the chip …
gnanam1990 Jul 29, 2026
0e17caa
feat(specialist): per-task stall watchdog (gap report §5.10)
gnanam1990 Jul 29, 2026
b499269
fix(specialist): resuming a task must not widen its authority
gnanam1990 Jul 29, 2026
7ccae3b
feat(tui): a plan run in the TUI is durable (gap report §5.2)
gnanam1990 Jul 29, 2026
fec3276
feat(config): the plan-size ceiling is a configurable tier (gap repor…
gnanam1990 Jul 29, 2026
ff9937e
feat(specialist): a stalled plan task is retried, and only a stalled …
gnanam1990 Jul 29, 2026
c67a12e
feat(tui): stop or pause a plan without stopping the turn (gap report…
gnanam1990 Jul 29, 2026
dcdbd02
fix(tui): the plan list follows the running task instead of pinning t…
gnanam1990 Jul 29, 2026
ddeb5ba
feat(specialist): save a plan that worked and run it again (gap repor…
gnanam1990 Jul 29, 2026
1fd568e
feat(specialist): resume a plan from where it stopped (gap report §5.…
gnanam1990 Jul 29, 2026
92f3f44
feat(specialist): ship one worked plan in the binary (gap report §5.14)
gnanam1990 Jul 29, 2026
5c36493
fix(cli): the model was never told the orchestrate tool exists
gnanam1990 Jul 29, 2026
ae7a16c
refactor(specialist): delete the orchestrate limits override, which n…
gnanam1990 Jul 29, 2026
8ce0d89
feat(tui): /plans restart runs the last plan from the beginning (gap …
gnanam1990 Jul 29, 2026
f7aeb9b
feat: run a plan in the background and report it on a later turn (gap…
gnanam1990 Jul 30, 2026
1d73e0d
fix(specialist): drop update_plan from the plan grant, which cost eve…
gnanam1990 Jul 30, 2026
f747fe8
feat(tui): a permission prompt can show what it is approving (write-t…
gnanam1990 Jul 30, 2026
f9d6579
feat(specialist): a write-capable plan gets a worktree, or does not r…
gnanam1990 Jul 30, 2026
7595c82
feat(specialist): a plan task may write, if it asks and is isolated (…
gnanam1990 Jul 30, 2026
844ce7e
fix(specialist): tell a plan task to USE its tools, not merely that i…
gnanam1990 Jul 30, 2026
fd4231b
feat(specialist): run independent plan tasks in parallel (gap report …
gnanam1990 Jul 30, 2026
a02420a
feat(tui): each plan task's progress lands on its own card (gap repor…
gnanam1990 Jul 30, 2026
e045428
test: prove resume survives a partial concurrent run (gap report §5.1…
gnanam1990 Jul 30, 2026
b762685
fix(credstore): serialize the credential read-modify-write across pro…
gnanam1990 Jul 30, 2026
f85d40a
fix(sandbox): a temporary grant is refcounted, so a sibling's cleanup…
gnanam1990 Jul 30, 2026
b0d96ac
fix(worktrees): a cancelled git dies as a process group, not just a s…
gnanam1990 Jul 30, 2026
65ba3f9
fix(specialist): an orchestrate approval must never be remembered
gnanam1990 Jul 30, 2026
dcd6d91
feat(tui): the posture chip leaves amber and shimmers under the cursor
gnanam1990 Jul 30, 2026
69815d2
feat(tui): the posture is a live word, not a coloured box
gnanam1990 Jul 30, 2026
2ae99dc
feat(tui): the posture reads lowercase and its hover flows
gnanam1990 Jul 30, 2026
4d163ac
fix(tui): a finished plan task must close its OWN card, not the last …
gnanam1990 Jul 30, 2026
2ea0ce6
feat(tui): one plan, one surface — the inline panel stands down for t…
gnanam1990 Jul 30, 2026
3e292c3
feat(tui): click an agent in AGENTS to open its brief, spend and reason
gnanam1990 Jul 30, 2026
d70a4bb
fix(tui): an agent with no card key must not expand itself and shove …
gnanam1990 Jul 30, 2026
7712751
fix(tui): the PLAN section holds both plans, so the footer line can f…
gnanam1990 Jul 30, 2026
f08ee30
fix(tui): a running task is work underway, not progress — the bar mus…
gnanam1990 Jul 30, 2026
197cdcc
feat(tui): a toggle for finished agents, and each one opens on what i…
gnanam1990 Jul 30, 2026
648c0d0
fix(tui): a sidebar row that was never drawn must not be clickable
gnanam1990 Jul 30, 2026
ac13ba3
fix(specialist): one plan per surface, on the path the model drives too
gnanam1990 Jul 30, 2026
0dfd185
test(specialist): measure concurrency against the host, not against t…
gnanam1990 Jul 30, 2026
72425c5
test(cli): neutralise XDG_CONFIG_HOME in the sandbox policy golden
gnanam1990 Jul 30, 2026
5e9a8a7
fix(tui): resume the named plan, sanitize task text, keep background …
Vasanthdev2004 Jul 31, 2026
9b6ea93
fix(sandbox): release a temporary root under one lock hold
Vasanthdev2004 Jul 31, 2026
4c31a74
fix(tui): guard the zeromaxing effort switch and two mouse hit-testers
Vasanthdev2004 Jul 31, 2026
cc3e9f9
fix(specialist): reject negative plan timeouts, correct two false claims
Vasanthdev2004 Jul 31, 2026
72d4a31
fix(specialist): make max_wall_seconds bound a concurrent plan
Vasanthdev2004 Jul 31, 2026
3b9f7b2
fix(specialist): let a task granted write tools actually use them
Vasanthdev2004 Jul 31, 2026
b8cd18d
feat(config): per-role plan model preferences, and cache counts a chi…
gnanam1990 Aug 1, 2026
e3336cf
feat(specialist): choose a model per task, prove it runs, and bound w…
gnanam1990 Aug 1, 2026
79da709
feat(cli): wire model discovery, proving and child scope to the live run
gnanam1990 Aug 1, 2026
c80a518
feat(tui): show which model ran a task, and what a plan left undone
gnanam1990 Aug 1, 2026
0b4ce8b
Merge origin/main into feat/zeromaxing
gnanam1990 Aug 1, 2026
1396d9e
fix(specialist): five admission and lifecycle defects from review
gnanam1990 Aug 1, 2026
e555fd5
feat(specialist): teach the verify convention, and show what happens …
gnanam1990 Aug 1, 2026
fb80aec
feat(specialist): give a plan with no bound at all a wall backstop, a…
gnanam1990 Aug 1, 2026
849d311
fix(config): project maxTurns may tighten only
gnanam1990 Aug 1, 2026
9447cca
feat(specialist): a plan no longer dies because its inputs were cut s…
gnanam1990 Aug 1, 2026
7aacde5
feat(specialist): reserve budget for later work, and declare the plan…
gnanam1990 Aug 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 135 additions & 0 deletions internal/agent/child_progress_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package agent

import (
"context"
"testing"

"github.com/Gitlawb/zero/internal/streamjson"
"github.com/Gitlawb/zero/internal/tools"
)

// progressProbeTool records whether the loop handed it a progress callback.
// streams mirrors what a real tool declares via tools.ChildProgressStreamer.
type progressProbeTool struct {
name string
streams bool
got *bool
}

func (t *progressProbeTool) Name() string { return t.name }
func (t *progressProbeTool) Description() string { return "probe" }
func (t *progressProbeTool) Parameters() tools.Schema {
return tools.Schema{Type: "object", Properties: map[string]tools.PropertySchema{}}
}
func (t *progressProbeTool) Safety() tools.Safety {
return tools.Safety{SideEffect: tools.SideEffectRead, Permission: tools.PermissionAllow}
}
func (t *progressProbeTool) Run(context.Context, map[string]any) tools.Result {
*t.got = false
return tools.Result{Status: tools.StatusOK, Output: "ok"}
}
func (t *progressProbeTool) RunWithOptions(_ context.Context, _ map[string]any, options tools.RunOptions) tools.Result {
*t.got = options.Progress != nil
return tools.Result{Status: tools.StatusOK, Output: "ok"}
}

// StreamsChildProgress makes this type implement tools.ChildProgressStreamer;
// the flag drives the answer. A tool that never declares at all is modelled by
// silentProbeTool below, which genuinely does not implement the interface.
func (t *progressProbeTool) StreamsChildProgress() bool { return t.streams }

// silentProbeTool does NOT implement tools.ChildProgressStreamer at all — the
// state every tool in the tree is in today except Task and orchestrate.
type silentProbeTool struct {
name string
got *bool
}

func (t *silentProbeTool) Name() string { return t.name }
func (t *silentProbeTool) Description() string { return "probe" }
func (t *silentProbeTool) Parameters() tools.Schema {
return tools.Schema{Type: "object", Properties: map[string]tools.PropertySchema{}}
}
func (t *silentProbeTool) Safety() tools.Safety {
return tools.Safety{SideEffect: tools.SideEffectRead, Permission: tools.PermissionAllow}
}
func (t *silentProbeTool) Run(context.Context, map[string]any) tools.Result {
*t.got = false
return tools.Result{Status: tools.StatusOK, Output: "ok"}
}
func (t *silentProbeTool) RunWithOptions(_ context.Context, _ map[string]any, options tools.RunOptions) tools.Result {
*t.got = options.Progress != nil
return tools.Result{Status: tools.StatusOK, Output: "ok"}
}

func runProbe(t *testing.T, tool tools.Tool, got *bool, withSink bool) bool {
t.Helper()
registry := tools.NewRegistry()
registry.Register(tool)
options := Options{}
if withSink {
options.OnToolProgress = func(string, streamjson.Event) {}
}
result, err := executeToolCall(context.Background(),
registry,
ToolCall{ID: "call_1", Name: tool.Name(), Arguments: "{}"},
PermissionModeUnsafe,
options)
if err != nil {
t.Fatalf("executeToolCall: %v", err)
}
if result.Status == "" {
t.Fatalf("probe produced no result")
}
return *got
}

// THE PARITY OBLIGATION for un-gating the progress path.
//
// The relationship asserted here is EQUALITY, per RULES.md §3: a tool's
// progress wiring before and after this change must be the same for every tool
// that already had it and every tool that already lacked it. Only a tool that
// newly DECLARES the interface may change.
//
// The name gate is gone, so the guarantee cannot be "Task still works" — it has
// to be stated in terms of the declaration, which is what these cases do.
func TestProgressCallbackFollowsTheDeclarationNotTheName(t *testing.T) {
t.Run("a declaring tool receives it (Task's behaviour, preserved)", func(t *testing.T) {
var got bool
// Named something other than "Task" ON PURPOSE: under the old name gate
// this case failed, which is the whole point of the change.
if !runProbe(t, &progressProbeTool{name: "spawner", streams: true, got: &got}, &got, true) {
t.Fatal("a tool declaring StreamsChildProgress must receive the callback")
}
})

t.Run("a tool named Task that does NOT declare gets nothing", func(t *testing.T) {
var got bool
// The inverse of the old behaviour, and the reason a name is the wrong
// key: identity is not capability.
if runProbe(t, &silentProbeTool{name: "Task", got: &got}, &got, true) {
t.Fatal("the callback must follow the declaration, not the name")
}
})

t.Run("a non-declaring tool receives nothing (every other tool, unchanged)", func(t *testing.T) {
var got bool
if runProbe(t, &silentProbeTool{name: "read_file", got: &got}, &got, true) {
t.Fatal("un-gating must not start handing a callback to tools that never had one")
}
})

t.Run("a declaring tool that answers false gets nothing", func(t *testing.T) {
var got bool
if runProbe(t, &progressProbeTool{name: "spawner", streams: false, got: &got}, &got, true) {
t.Fatal("StreamsChildProgress() == false must be honoured")
}
})

t.Run("no sink means no callback, whatever the tool declares", func(t *testing.T) {
var got bool
if runProbe(t, &progressProbeTool{name: "spawner", streams: true, got: &got}, &got, false) {
t.Fatal("without OnToolProgress there is nothing to forward to")
}
})
}
13 changes: 13 additions & 0 deletions internal/agent/export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
package agent

import (
"github.com/Gitlawb/zero/internal/specialist"
"github.com/Gitlawb/zero/internal/tools"
"github.com/Gitlawb/zero/internal/zeroruntime"
)
Expand Down Expand Up @@ -39,3 +40,15 @@ func parsePreservedState(summaryContent string) (string, []skillEntry) {
func partitionTools(registry *tools.Registry, permissionMode PermissionMode, options Options, loaded map[string]bool) ([]zeroruntime.ToolDefinition, string) {
return partitionToolsCached(registry, permissionMode, options, loaded, nil)
}

// Phase 2 additivity-proof seam. The identity test uses the REAL orchestrate
// tool rather than a stub, so it exercises the actual Deferred() contract that
// enforces the posture-off constraint. internal/specialist does not import
// internal/agent, so this direction creates no cycle.
const phase2ToolName = specialist.OrchestrateToolName

// registerPhase2ToolForTest registers the real tool with the posture OFF, which
// is the condition the identity test is about.
func registerPhase2ToolForTest(registry *tools.Registry) {
registry.Register(&specialist.OrchestrateTool{PostureActive: func() bool { return false }})
}
53 changes: 48 additions & 5 deletions internal/agent/loop.go
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,19 @@ func Run(ctx context.Context, prompt string, provider Provider, options Options)
for turn := 0; turn < maxTurns; turn++ {
result.Turns = turn + 1

// The zeromaxing posture's reminders. Appended to the CONVERSATION tail
// as user-role messages — the same channel as the diagnostics nudge
// below and the failure/plan hints later in the turn — and never into
// the system prompt, which is built once per run and must stay
// byte-stable so the provider's cached prefix survives. See
// internal/agent/zeromaxing.go.
for _, reminder := range zeromaxingReminders(options.Zeromaxing, result.Turns, options.OrchestrateAvailable) {
messages = append(messages, zeroruntime.Message{
Role: zeroruntime.MessageRoleUser,
Content: reminder,
})
}

// Deliver background post-edit diagnostics from the previous turn's edits
// BEFORE compaction so the nudge is part of the request being budgeted.
// A brief wait at most (asyncDiagnosticsDrainTimeout); an unfinished check
Expand All @@ -267,6 +280,19 @@ func Run(ctx context.Context, prompt string, provider Provider, options Options)
})
}

// A BACKGROUND PLAN that finished since the last turn. Same channel and
// the same point in the turn as the diagnostics nudge above: the model
// was told the plan was not finished and must not report it as done, so
// this is the message that makes that promise good.
if options.PlanCompletions != nil {
if finished := options.PlanCompletions(); finished != "" {
messages = append(messages, zeroruntime.Message{
Role: zeroruntime.MessageRoleUser,
Content: finished,
})
}
}

// Build the per-turn tool list first so proactive compaction can include
// the tool-definition tokens (they ride on every request) in its estimate.
// partitionTools depends only on registry/permissions/options/loaded, not on
Expand Down Expand Up @@ -1333,10 +1359,17 @@ func executeToolCall(ctx context.Context, registry *tools.Registry, call ToolCal
}
args = shellExecutionArgsForApproval(call.Name, args, decisionAction, options)

// Task tool: wire progress callback so the TUI sees live tool-call events
// from the specialist child process.
// Wire the progress callback so the TUI sees live tool-call events from a
// child agent process.
//
// Keyed on the TOOL'S OWN DECLARATION (tools.ChildProgressStreamer), not on
// its name. This was `call.Name == "Task"`, which made the second
// sub-agent-spawning tool run invisibly; `|| call.Name == "orchestrate"`
// would have been the same defect one name later. A tool that spawns
// children declares it, and every tool that does not keeps the nil callback
// it has today.
var progressCallback func(streamjson.Event)
if call.Name == "Task" && options.OnToolProgress != nil {
if options.OnToolProgress != nil && tools.StreamsChildProgress(tool) {
toolCallID := call.ID
onProgress := options.OnToolProgress
progressCallback = func(event streamjson.Event) {
Expand Down Expand Up @@ -2710,7 +2743,7 @@ func availablePermissionDecisions(event PermissionEvent, args map[string]any, op
decisions = append(decisions, PermissionDecisionAlwaysAllowPrefix)
}
}
if options.Sandbox.CanPersistGrants() && permissionSupportsPersistentDecision(event.ToolName) && !filesystemSandboxPrompt(event) && !inlineAdditionalPermissions {
if options.Sandbox.CanPersistGrants() && permissionSupportsPersistentDecision(event.ToolName, options) && !filesystemSandboxPrompt(event) && !inlineAdditionalPermissions {
decisions = append(decisions, PermissionDecisionAlwaysAllow)
}
}
Expand Down Expand Up @@ -2881,7 +2914,17 @@ func grantFilesystemForSandboxPrompt(event PermissionEvent, scope sandbox.Permis
}, scope)
}

func permissionSupportsPersistentDecision(toolName string) bool {
func permissionSupportsPersistentDecision(toolName string, options Options) bool {
// THE TOOL'S OWN DECLARATION FIRST. A name list cannot describe a tool whose
// reach depends on its arguments, and it cannot cover a tool that RUNS the
// ones already refused below — see tools.PersistentPermissionRefuser.
if options.Registry != nil {
if tool, found := options.Registry.Get(toolName); found {
if refuser, ok := tool.(tools.PersistentPermissionRefuser); ok && refuser.RefusesPersistentPermission() {
return false
}
}
}
switch toolName {
case "bash", "exec_command", "write_stdin", "apply_patch":
return false
Expand Down
134 changes: 134 additions & 0 deletions internal/agent/loop_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3756,3 +3756,137 @@ func TestRunNilTraceForwardsUsage(t *testing.T) {
t.Fatal("OnUsage not forwarded when Trace is nil")
}
}

// (h) The zeromaxing variant of TestRunPreservesRequestPrefixAcrossTurns.
//
// This is the test the whole reminder design exists to satisfy. The system
// prompt and tool definitions are the provider's CACHED PREFIX; #760 made them
// build once per run so they stay byte-identical across turns. The posture adds
// per-turn text, so it is exactly the kind of change that silently destroys the
// cache — roughly doubling input cost with nothing detecting it.
//
// The reminders are appended to the conversation TAIL as user-role messages, so
// each turn's request must still be an exact prefix-extension of the previous
// one, with identical tools and cache key.
func TestRunPreservesRequestPrefixAcrossTurnsUnderZeromaxing(t *testing.T) {
root := t.TempDir()
writeAgentTestFile(t, filepath.Join(root, "notes.txt"), "alpha\n")
registry := tools.NewRegistry()
registry.Register(tools.NewReadFileTool(root))
provider := &mockProvider{turns: [][]zeroruntime.StreamEvent{
{
{Type: zeroruntime.StreamEventToolCallStart, ToolCallID: "call-1", ToolName: "read_file"},
{Type: zeroruntime.StreamEventToolCallDelta, ToolCallID: "call-1", ArgumentsFragment: `{"path":"notes.txt"}`},
{Type: zeroruntime.StreamEventToolCallEnd, ToolCallID: "call-1"},
{Type: zeroruntime.StreamEventDone},
},
{
{Type: zeroruntime.StreamEventToolCallStart, ToolCallID: "call-2", ToolName: "read_file"},
{Type: zeroruntime.StreamEventToolCallDelta, ToolCallID: "call-2", ArgumentsFragment: `{"path":"notes.txt"}`},
{Type: zeroruntime.StreamEventToolCallEnd, ToolCallID: "call-2"},
{Type: zeroruntime.StreamEventDone},
},
{
{Type: zeroruntime.StreamEventText, Content: "done"},
{Type: zeroruntime.StreamEventDone},
},
}}

if _, err := Run(context.Background(), "read notes", provider, Options{
Cwd: root,
Registry: registry,
SessionID: "session-stable-prefix-zeromaxing",
Zeromaxing: ZeromaxingEntering,
}); err != nil {
t.Fatal(err)
}
if len(provider.requests) != 3 {
t.Fatalf("provider requests = %d, want 3", len(provider.requests))
}

// Every request must extend the previous one exactly — nothing rewritten
// above the append point, which is what keeps the cached prefix valid.
for i := 1; i < len(provider.requests); i++ {
prev, cur := provider.requests[i-1], provider.requests[i]
if len(cur.Messages) < len(prev.Messages) ||
!reflect.DeepEqual(cur.Messages[:len(prev.Messages)], prev.Messages) {
t.Fatalf("request %d is not an exact prefix-extension of request %d:\nprev=%#v\ncur=%#v",
i, i-1, prev.Messages, cur.Messages)
}
if !reflect.DeepEqual(prev.Tools, cur.Tools) {
t.Fatalf("tool definitions drifted between turns %d and %d", i-1, i)
}
if cur.PromptCacheKey != prev.PromptCacheKey {
t.Fatalf("prompt cache key drifted: %q -> %q", prev.PromptCacheKey, cur.PromptCacheKey)
}
}

// The system prompt — the cached prefix itself — must be byte-identical
// across turns, and must contain NONE of the posture text.
system := provider.requests[0].Messages[0]
for i, request := range provider.requests {
if !reflect.DeepEqual(request.Messages[0], system) {
t.Fatalf("system message changed on turn %d — the cached prefix is broken", i+1)
}
}
for _, forbidden := range []string{
ZeromaxingEnterNotice, ZeromaxingBudgetNotice, ZeromaxingStillOnNotice, ZeromaxingExitNotice,
} {
if strings.Contains(system.Content, forbidden) {
t.Fatalf("posture reminder leaked into the SYSTEM PROMPT (above the cache breakpoint): %q", forbidden)
}
}

// ...and the reminders really did arrive, on schedule. Without this the
// assertions above would pass just as happily if the feature did nothing.
firstTurn := renderZeromaxingMessages(provider.requests[0].Messages)
if !strings.Contains(firstTurn, ZeromaxingEnterNotice) || !strings.Contains(firstTurn, ZeromaxingBudgetNotice) {
t.Fatalf("turn 1 must carry the enter + budget notices:\n%s", firstTurn)
}
if strings.Contains(firstTurn, ZeromaxingStillOnNotice) {
t.Fatalf("turn 1 must NOT carry the still-on notice:\n%s", firstTurn)
}
secondOnly := renderZeromaxingMessages(provider.requests[1].Messages[len(provider.requests[0].Messages):])
if !strings.Contains(secondOnly, ZeromaxingStillOnNotice) {
t.Fatalf("turn 2 must carry the still-on notice:\n%s", secondOnly)
}
if strings.Contains(secondOnly, ZeromaxingEnterNotice) {
t.Fatalf("turn 2 must NOT re-announce entry:\n%s", secondOnly)
}
}

// A run with the posture OFF must be byte-identical to one that never heard of
// it — the no-regression half of the feature.
func TestRunWithoutZeromaxingCarriesNoPostureText(t *testing.T) {
root := t.TempDir()
registry := tools.NewRegistry()
registry.Register(tools.NewReadFileTool(root))
provider := &mockProvider{turns: [][]zeroruntime.StreamEvent{
{{Type: zeroruntime.StreamEventText, Content: "done"}, {Type: zeroruntime.StreamEventDone}},
}}
if _, err := Run(context.Background(), "hello", provider, Options{
Cwd: root, Registry: registry, SessionID: "session-no-zeromaxing",
}); err != nil {
t.Fatal(err)
}
rendered := renderZeromaxingMessages(provider.requests[0].Messages)
for _, forbidden := range []string{
ZeromaxingEnterNotice, ZeromaxingBudgetNotice, ZeromaxingStillOnNotice, ZeromaxingExitNotice,
} {
if strings.Contains(rendered, forbidden) {
t.Fatalf("a posture-free run must carry no posture text, found %q", forbidden)
}
}
}

// renderZeromaxingMessages flattens messages to one searchable string.
func renderZeromaxingMessages(messages []zeroruntime.Message) string {
var b strings.Builder
for _, message := range messages {
b.WriteString(string(message.Role))
b.WriteString(": ")
b.WriteString(message.Content)
b.WriteString("\n")
}
return b.String()
}
Loading
Loading