fix: address review + green CI for trail context injection · Entire

fix: address review + green CI for trail context injection

1144985→main·

dipree·1mo ago·6 files·+39 added/-93 removed

- Track once-per-session injection on session.State (ContextInjectionDecided) instead of a separate .injected marker file. The marker lived in .git/entire-sessions/ and broke integration tests that count dir entries and read stateFiles[0] (it sorted before .json). Removes ClaimContextInjection. - Bound NewAuthenticatedAPIClient under the same probe timeout as the HTTP call (it can do /.well-known discovery + token exchange) — Copilot review. - Drain the TrailsEnabled response body before close for connection reuse — Copilot review. - gofmt inject_test.go (lint).

Sessions

2df0d7859de4View transcript

[?
Inject Trail Context into Agent ModelPi·Opus 4.8·1 step](/content/gh/entireio/cli/session/019ecbec-8bcf-7f35-a647-2fc26c08a22e#timeline-2df0d7859de4/index.html)

Changes

6

51 unmodified lines
type injectorStub struct{ Agent }

func (injectorStub) InjectionEvent() EventType { return TurnStart }

//nolint:unparam // signature is dictated by the ContextInjector interface
func (injectorStub) RenderContextInjection(ContextInjection) ([]byte, error) {
    return []byte("x"), nil
}

Mcmd/entire/cli/agent/inject_test.go+1

2 unmodified lines
import (
    "context"
    "fmt"
    "io"
    "net/http"
)
11 unmodified lines
    
    return false, fmt.Errorf("probe trails enablement: %w", err)
}

defer resp.Body.Close()
// Drain (bounded) so net/http can reuse the connection; the body is unused.
_, _ = io.Copy(io.Discard, io.LimitReader(resp.Body, 1<<16)) //nolint:errcheck // best-effort drain
return resp.StatusCode >= http.StatusOK && resp.StatusCode < http.StatusMultipleChoices, nil
}

Mcmd/entire/cli/api/trails.go+3

// trailsEnabledForRepo reports whether Entire trails are literally enabled for
// this repo on the API. It resolves the origin remote to a supported forge and
// probes the trails endpoint; a successful response means trails are
// provisioned/enabled. Best-effort and bounded by a short timeout — an
// unresolved remote, missing auth, or any API/transport error reports false, so
// we never advertise trails we can't confirm are enabled.
func trailsEnabledForRepo(ctx context.Context) bool {
    forge, owner, repo, err := resolveTrailRemote(ctx)
    if err != nil {
        return false
    }
    client, err := NewAuthenticatedAPIClient(ctx, false)
    if err != nil {
        return false // not authenticated → trails aren't enabled for us
    }
    probeCtx, cancel := context.WithTimeout(ctx, trailsEnabledProbeTimeout)
    defer cancel()
    client, err := NewAuthenticatedAPIClient(probeCtx, false)
    if err != nil {
        return false // not authenticated → trails aren't enabled for us
    }
    enabled, err := client.TrailsEnabled(probeCtx, forge, owner, repo)
    return err == nil && enabled
}

Mcmd/entire/cli/lifecycle.go+28/-17

224 unmodified lines
// ClaimContextInjection records that Entire's model-facing context injection
// has been emitted for a session, so the injection happens at most once per
// session. First-writer-wins, mirroring ClaimSessionStartBanner (and cleaned up
// by ClearSessionState, which removes every "<sessionID>" file).
//
// Returns (claimed=true) when this call won the race and the caller should emit
// the injection; (claimed=false) when an earlier call already claimed it.
func ClaimContextInjection(ctx context.Context, sessionID string) (claimed bool, err error) {
    if vErr := validation.ValidateSessionID(sessionID); vErr != nil {
        return false, fmt.Errorf("invalid session ID: %w", vErr)
    }

root, rErr := openSessionStateRoot(ctx)
    if rErr != nil {
        return false, rErr
    }
    defer root.Close()

f, oErr := root.OpenFile(sessionID+".injected", os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o600)
    if oErr != nil {
        if errors.Is(oErr, os.ErrExist) {
            return false, nil
        }
        return false, fmt.Errorf("failed to create injection marker file: %w", oErr)
    }
    _ = f.Close()
    return true, nil
}

Mcmd/entire/cli/session/state.go+7

372 unmodified lines
// LoadAgentTypeHint reads the agent type hint written by SessionStart.
// Returns empty string if the hint file doesn't exist, can't be read, or the
// value isn't a registered agent type.

Mcmd/entire/cli/strategy/session_state.go-29

997 unmodified lines
    }
}

func TestClaimContextInjection_FirstWriterWins(t *testing.T) {
    dir := t.TempDir()
    _, err := git.PlainInit(dir, false)
    require.NoError(t, err)
    t.Chdir(dir)

ctx := context.Background()
    sessionID := "2026-01-01-injection-claim"

claimed, err := ClaimContextInjection(ctx, sessionID)
    require.NoError(t, err)
    require.True(t, claimed, "first call must win the injection claim")

claimed, err = ClaimContextInjection(ctx, sessionID)
    require.NoError(t, err)
    require.False(t, claimed, "subsequent calls must report the injection already claimed")
}

func TestClaimContextInjection_InvalidSessionID_ReturnsError(t *testing.T) {
    dir := t.TempDir()
    _, err := git.PlainInit(dir, false)
    require.NoError(t, err)
    t.Chdir(dir)

_, err = ClaimContextInjection(context.Background(), "../../../etc/passwd")
    require.Error(t, err)
}

func TestClearSessionState_RemovesInjectionMarker(t *testing.T) {
    dir := t.TempDir()
    _, err := git.PlainInit(dir, false)
    require.NoError(t, err)
    t.Chdir(dir)

ctx := context.Background()
    sessionID := "2026-01-01-clear-injection"

_, err = ClaimContextInjection(ctx, sessionID)
    require.NoError(t, err)
    require.NoError(t, ClearSessionState(ctx, sessionID))

// After clear, the marker is gone — the next claim wins again.
    claimed, err := ClaimContextInjection(ctx, sessionID)
    require.NoError(t, err)
    require.True(t, claimed, "ClearSessionState should remove the injection marker")
}

Mcmd/entire/cli/strategy/session_state_test.go-47