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:
- Document piSkillEvents: Pi uses live capture; PiAgent must NOT implement SkillEventExtractor, because condensation merges extractor output with the live events via mergeSkillEvents and the keys cannot dedup cleanly across the live/transcript boundary (per-invocation vs current TurnID), which would double-count earlier-turn skills in checkpoint metadata.
- Add TestPiAgent_UsesLiveSkillCaptureNotTranscriptExtraction: fails if a future change makes PiAgent a SkillEventExtractor, with guidance.
- Add TestParseHookEvent_BeforeAgentStart_MultipleSkillEvents: locks multi-invocation live capture.
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
cmd/entire/cli/agent/pi
Mlifecycle.go+15
Mlifecycle_test.go+49
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{}