Lock Pi's live skill-capture model; guard against duplicate extractor · Entire

Lock Pi's live skill-capture model; guard against duplicate extractor

9854c62→main·

dipree·1mo ago·2 files·+64 added/-0 removed

Pi already captures invoked /skill:<name> commands in every live session (including entire review --agent pi) via the extension's input handler → before_agent_start hook payload → state.SkillEvents. This is Pi's only skill-capture path and is the counterpart to claude-code's transcript- extraction model — the two are mutually exclusive.

This change documents and protects that decision rather than adding a (harmful) transcript extractor:

Sessions

da693850a5cbView transcript

[?
List me all the potential command combinations for review.Pi·Opus 4.8·1 step](/content/gh/entireio/cli/session/019e9fb5-004d-7145-8f48-db6a240d0ab3#timeline-da693850a5cb/index.html)

Changes

2

69 unmodified lines

70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90

69 unmodified lines

Timestamp  string `json:"timestamp,omitempty"`
}

// piSkillEvents converts the Pi extension's live skill-invocation reports into
// agent.SkillEvents. This is Pi's ONLY skill-capture path, and it is captured
// live: the embedded extension matches "/skill:<name>" on raw input (before Pi
// expands it into a <skill name="..."> block) and ships the result in the
// before_agent_start hook payload. These events flow into state.SkillEvents and
// are merged into checkpoint metadata at condensation.
//
// Deliberately, PiAgent does NOT implement agent.SkillEventExtractor (the
// transcript-extraction interface that claude-code uses). The two models are
// mutually exclusive on purpose — see TestPiAgent_UsesLiveSkillCaptureNotTranscriptExtraction.
// Adding a transcript extractor here would double-count: condensation merges
// the extractor output with these live events via mergeSkillEvents, and the
// keys cannot dedup cleanly (live events carry their original per-invocation
// TurnID, while a re-extraction stamps every event with the current TurnID),
// so earlier-turn skills would surface twice in checkpoint metadata.
func piSkillEvents(in []piSkillEventInput) []agent.SkillEvent {
    if len(in) == 0 {
        return nil

Mcmd/entire/cli/agent/pi/lifecycle.go+15

70 unmodified lines

71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125

70 unmodified lines

}
}

// TestParseHookEvent_BeforeAgentStart_MultipleSkillEvents locks the live-capture
// guarantee for more than one invocation in a single turn: every reported
// /skill:<name> must surface as its own SkillEvent. This is the path that backs
// invoked-skill capture for ALL live Pi sessions, including `entire review
// --agent pi`.
func TestParseHookEvent_BeforeAgentStart_MultipleSkillEvents(t *testing.T) {
    t.Parallel()
    a := &PiAgent{}
    stdin := strings.NewReader(`{"type":"before_agent_start","session_file":
"/tmp/2026-05-09T12-00-00-000Z_abc-123.jsonl","prompt":"expanded","skill_events":[{"skill_name":"review-pr","invocation":"/skill:review-pr","timestamp":"2026-05-25T12:34:56Z"},{"skill_name":"test-auditor","invocation":"/skill:test-auditor","timestamp":"2026-05-25T12:35:10Z"}]}`)
    ev, err := a.ParseHookEvent(context.Background(), HookNameBeforeAgentStart, stdin)
    if err != nil {
        t.Fatalf("ParseHookEvent: %v", err)
    }
    got := make(map[string]bool)
    for _, se := range ev.SkillEvents {
        got[se.Skill.Name] = true
        if se.Source.Agent != string(agent.AgentNamePi) {
            t.Errorf("Source.Agent = %q, want pi", se.Source.Agent)
        }
    }
    for _, want := range []string{"review-pr", "test-auditor"} {
        if !got[want] {
            t.Errorf("missing live skill event for %q; got %v", want, got)
        }
    }
}

// TestPiAgent_UsesLiveSkillCaptureNotTranscriptExtraction is an architectural
// guard. Pi captures invoked skills LIVE via the extension's input handler
// (see piSkillEvents), which is mutually exclusive with the transcript-
// extraction model claude-code uses. PiAgent must therefore NOT implement
// agent.SkillEventExtractor: condensation merges any extractor output with the
// live state.SkillEvents, and the keys cannot dedup cleanly across the
// live/transcript boundary (per-invocation vs current TurnID), so adding an
// extractor would double-count earlier-turn skills in checkpoint metadata.
//
// If you are here because this test failed: you likely added an ExtractSkillEvents
// method to PiAgent. Don't — Pi already captures invoked skills live. Reconcile
// the dedup model first (make skill-event identity stable across the boundary)
// before reintroducing transcript extraction.
func TestPiAgent_UsesLiveSkillCaptureNotTranscriptExtraction(t *testing.T) {
    t.Parallel()
    if _, ok := agent.AsSkillEventExtractor(NewPiAgent()); ok {
                t.Fatal("PiAgent implements SkillEventExtractor: Pi uses live skill capture; " +
                "a transcript extractor would double-count live-captured events at condensation. " +
                "See piSkillEvents and this test's doc comment.")
    }
}

func TestParseHookEvent_SessionShutdown_NoLifecycleEvent(t *testing.T) {
    t.Parallel()
    a := &PiAgent{}