Documentation
¶
Overview ¶
Package harness detects which agent harness (claude/codex/...) is running in a wrapped PTY and whether it is working or idle, so the terminal-share heartbeat can stamp live agent info instead of relying on launch-time env vars. Detection is pure and platform-free: the wrapper feeds in foreground-process snapshots and byte-activity timestamps; this package never touches the PTY itself.
The design (and the timing constants) are ported from herdr's pane detection: identification is process-tree based, working/idle is PTY byte-causality based. See docs/specs/agent-detection.md.
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Identify ¶
Identify maps a foreground process (comm + full argv, argv[0] included) to a canonical harness name, or "" when the process is not a known harness. It normalizes names (case, .exe/.cmd/.bat/.ps1/.js suffixes, whitespace) and sees through generic runtimes (node, python, sh/bash/zsh, pwsh) by scanning argv for a known binary name.
Types ¶
type ActivityTracker ¶
type ActivityTracker struct {
// contains filtered or unexported fields
}
ActivityTracker classifies a wrapped harness as working or idle from PTY byte causality alone — no screen parsing. Output observed while no recent user input is in flight marks the harness working for outputActivityWindow; quiescence past the window reads as idle. Callers pass `now` explicitly so tests never sleep.
Not safe for concurrent use; the wrapper serializes observations onto its detection goroutine.
func NewActivityTracker ¶
func NewActivityTracker(start time.Time) *ActivityTracker
NewActivityTracker starts tracking for a harness identified at `start`. The first startupGraceWindow after start reads as idle regardless of output.
func (*ActivityTracker) ObserveInput ¶
func (t *ActivityTracker) ObserveInput(now time.Time)
ObserveInput records user-originated input at `now` (local stdin, remote .stdin messages, alert auto-submission).
func (*ActivityTracker) ObserveOutput ¶
func (t *ActivityTracker) ObserveOutput(now time.Time)
ObserveOutput records PTY output bytes from the child at `now`. Output inside the startup grace is boot noise, and output inside the input-taint window is presumed local echo / prompt redraw; neither counts as agent work.
type Detection ¶
type Detection struct {
Harness string // canonical harness name, "" when none
ChildPID int // foreground pid when a harness is identified
State State // StateUnknown when Harness is ""
}
Detection is the point-in-time snapshot the heartbeat loop stamps into payloads. Zero value means "no harness in the foreground".
type Detector ¶
type Detector struct {
// contains filtered or unexported fields
}
Detector owns harness detection for one wrapped PTY: it polls the foreground process via the injected inspector and folds byte observations into an ActivityTracker. Inspect is the platform seam — TIOCGPGRP + process lookup in production, a fake in tests.
Inspect errors retain the previous identification (a transient ps failure must not flap `ppz who`); a successful inspect of a non-harness clears it. A harness change resets the activity tracker so each identification gets its own startup grace.
Safe for concurrent use: the wrapper calls Observe* from its PTY I/O goroutines, Poll from the detection ticker, and Snapshot from the heartbeat loop.
func NewDetector ¶
func NewDetector(inspect func() (ForegroundProc, error)) *Detector
NewDetector creates a Detector around the given inspector. Activity tracking (and its startup grace) is keyed to identification time — the Poll that first sees a harness — not to PTY spawn time, so the detector needs no clock input of its own.
func (*Detector) ObserveInput ¶
ObserveInput forwards user input activity to the tracker.
func (*Detector) ObserveOutput ¶
ObserveOutput forwards PTY output activity to the tracker.
func (*Detector) SetScreen ¶ added in v0.48.0
SetScreen wires an optional visible-screen source (the wrapper's live screen model, bottom lines). When set, an identified harness whose byte causality says "not working" is further arbitrated by its ScreenDetector: blocker chrome on screen → StateBlocked. The source is called with d's lock held and must not call back into d.
type ForegroundProc ¶
ForegroundProc is what the platform inspector reports about the PTY's foreground process group: the group leader's pid, its comm (binary name), and full argv (argv[0] included).
type ScreenDetector ¶ added in v0.48.0
type ScreenDetector interface {
// Blocked reports whether the visible screen content (the bottom
// lines of the live screen model) shows the harness waiting on
// human input.
Blocked(content string) bool
}
ScreenDetector is the phase-3 extension point: a per-harness matcher over the visible terminal screen that recognizes "waiting on human input" (permission dialogs, forms, choice prompts). It is consulted only when byte causality already says the harness is not working — PTY activity remains the authority for working, the screen only arbitrates idle vs blocked (same division of authority as herdr).
func ScreenDetectorFor ¶ added in v0.48.0
func ScreenDetectorFor(name string) ScreenDetector
ScreenDetectorFor returns the screen detector for a canonical harness name, or nil when that harness has no screen patterns yet — nil simply means the harness never reports blocked. Phase 3 ships Claude Code first; adding another harness is one case here plus one screen_<name>.go pattern file (herdr's per-agent module layout).
type Spec ¶
Spec describes one known harness. Extending detection to a new harness is one new row here. Names are the canonical ids `ppz agent --harness` already uses, so heartbeat consumers see one vocabulary regardless of whether the value came from env or detection.