daemon

package
v0.20.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 5, 2026 License: MIT Imports: 35 Imported by: 0

README

Background Daemon

A long-running background service that holds your tracker credentials once and answers human commands for you, so individual commands run instantly without re-authenticating each time.

  • Runs human commands without per-call setup
  • Holds tracker tokens once on the host
  • Routes commands to the right project automatically
  • Fetches issues from all configured trackers
  • Completes browser OAuth sign-in flows automatically
  • Surfaces ready-for-review handoffs to the TUI
  • Pauses for confirmation on destructive operations
  • Records tool activity for later statistics
  • Derives each PM ticket's workflow-board placement and applies drag-to-trigger pipeline transitions for the desktop board

Documentation

Index

Constants

View Source
const (
	PlanningStartedHeader       = "[human:planning-started]"
	PlanReadyHeader             = "[human:plan-ready]"
	PlanningFailedHeader        = "[human:planning-failed]"
	ImplementationStartedHeader = "[human:implementation-started]"
	ImplementationFailedHeader  = "[human:implementation-failed]"
	ReviewStartedHeader         = "[human:review-started]"
	ReviewFailedHeader          = "[human:review-failed]"
	PRStartedHeader             = "[human:pr-started]"
	PRPushedHeader              = "[human:pr-pushed]"
	PRFailedHeader              = "[human:pr-failed]"
)

Board marker headers. These mirror the existing review-handoff headers in review_handoff.go and follow the same `strings.HasPrefix(trimmed, header)` contract: a comment that merely quotes a header mid-body is NOT a marker.

ReadyForReviewHeader is reused as the implementation done-marker and ReviewCompleteHeader as the verification done-marker; both are declared in review_handoff.go and intentionally NOT redeclared here.

View Source
const (
	// DefaultPort is the well-known daemon listening port.
	DefaultPort = 19285
	// DefaultChromePort is the well-known Chrome proxy port.
	DefaultChromePort = 19286
	// DefaultProxyPort is the well-known HTTPS proxy port.
	DefaultProxyPort = 19287

	// DockerHost is the hostname Docker provides for reaching the host machine
	// from inside a container. Enabled by --add-host=host.docker.internal:host-gateway.
	DockerHost = "host.docker.internal"
)
View Source
const ReadyForReviewHeader = "[human:ready-for-review]"

ReadyForReviewHeader is the single-line magic header that identifies a review handoff in a PM-ticket comment. See cli/CLAUDE.md "Review handoff".

View Source
const ReviewCompleteHeader = "[human:review-complete]"

ReviewCompleteHeader is the matching follow-up header posted after the reviewer agent finishes. Presence of a *newer* review-complete comment clears the ready-for-review flag, so the TUI stops showing (R) once a review has actually landed.

Variables

This section is empty.

Functions

func BoardTransition added in v0.20.0

func BoardTransition(addr, token string, req BoardTransitionRequest) error

BoardTransition asks the daemon to advance a card one pipeline stage. The request is sent as a single JSON arg so multi-word PM titles survive arg splitting on the daemon side.

func ClassifyMarker added in v0.20.0

func ClassifyMarker(body string) (BoardStage, BoardState, bool)

ClassifyMarker reports the stage and state a comment body represents and whether it is a recognized board marker at all. A body is only a marker when it STARTS with a known header (after trimming), so a quoted header in the middle of a discussion comment does not register. Pure: no I/O.

func ConnectedPath

func ConnectedPath() string

ConnectedPath returns the default path for the connected PIDs file.

func GenerateToken

func GenerateToken() (string, error)

GenerateToken returns a cryptographically random 32-byte hex string.

func GetHookSnapshot

func GetHookSnapshot(addr, token string) (map[string]hookevents.SessionSnapshot, error)

GetHookSnapshot fetches the current per-session hook state from the daemon.

func GetLogMode

func GetLogMode(addr, token string) (string, error)

GetLogMode fetches the current traffic log mode from the daemon.

func GetToolStats

func GetToolStats(addr, token string) (*stats.ToolStats, error)

GetToolStats fetches pre-aggregated tool call statistics from the daemon.

func GetTrackerDiagnose

func GetTrackerDiagnose(addr, token string) ([]tracker.TrackerStatus, error)

GetTrackerDiagnose fetches tracker credential status from the daemon.

func InfoPath

func InfoPath() string

InfoPath returns the default path for the daemon info file (~/.human/daemon.json).

func IsProcessAlive

func IsProcessAlive(pid int) bool

IsProcessAlive checks whether a process with the given PID is still running.

func IsReviewComplete

func IsReviewComplete(body string) bool

IsReviewComplete reports whether the comment body is a review-complete follow-up, which supersedes any earlier handoff for the same engineering keys.

func LoadOrCreateToken

func LoadOrCreateToken() (string, error)

LoadOrCreateToken reads the token from disk, or generates and persists a new one.

func LogPath

func LogPath() string

LogPath returns the path to the daemon log file (~/.human/daemon.log).

func ParseEngineeringKeysFromHandoff

func ParseEngineeringKeysFromHandoff(body string) []string

ParseEngineeringKeysFromHandoff extracts the engineering ticket keys listed on the `engineering:` line of a [human:ready-for-review] comment body. Returns nil if the body is not a handoff block or has no engineering line.

The comment body must START with ReadyForReviewHeader so a comment that merely quotes the header (e.g. in a discussion) does not trigger a handoff.

func ParseHookEventArgs

func ParseHookEventArgs(args []string) hookevents.Event

ParseHookEventArgs converts daemon request args into a hook event. Expected args: [event, session_id, cwd, notification_type, tool_name, error_type, agent_name].

Every field is length-capped and Cwd must be absolute — both as defence against abusive clients that could otherwise poison the in-memory hook store or write relative paths that collide with registered project directories.

func ParsePRFromHandoff added in v0.20.0

func ParsePRFromHandoff(body string) string

ParsePRFromHandoff extracts the pull-request URL from the optional `pr:` line of a [human:ready-for-review] comment body. Returns "" when the body is not a handoff block or carries no pr: line (the line is optional — handoffs from flows that only push a branch omit it).

func PidPath

func PidPath() string

PidPath returns the path to the daemon PID file (~/.human/daemon.pid).

func QueryAudit added in v0.20.0

func QueryAudit(addr, token string, filterArgs []string) ([]audit.Event, error)

QueryAudit reads audit events from the daemon (which owns the audit DB), forwarding the pre-parsed filter flags. filterArgs is the slice of --since/--until/--subject/--tracker/--limit tokens.

func ReadAlivePid

func ReadAlivePid() (int, bool)

ReadAlivePid reads the PID file and checks if the process is alive. Returns (0, false) if no PID file exists or the process is dead.

func ReadConnected

func ReadConnected(path string) []int

ReadConnected reads connected PIDs from path. Returns nil on any error.

func RemoveConnected

func RemoveConnected(path string)

RemoveConnected removes the connected PIDs file (best-effort).

func RemoveInfo

func RemoveInfo()

RemoveInfo removes the daemon info file (best-effort).

func RemovePidFile

func RemovePidFile()

RemovePidFile removes the PID file (best-effort).

func RunAgentCleanup

func RunAgentCleanup(ctx context.Context, store *HookEventStore, cleaner AgentCleaner, logger zerolog.Logger)

RunAgentCleanup watches for SessionEnd hook events from devcontainer agents and automatically stops the container and removes the worktree.

func RunAgentZombieSweep

func RunAgentZombieSweep(ctx context.Context, sweeper AgentZombieSweeper, logger zerolog.Logger)

RunAgentZombieSweep periodically checks for agent containers that are still running but have no Claude process. This catches cases where Claude failed to start, crashed without firing hook events, or the user killed the tmux pane.

func RunBoardFailureWatch added in v0.20.0

func RunBoardFailureWatch(ctx context.Context, store *HookEventStore, commenterFor func() (tracker.Commenter, error), logger zerolog.Logger)

RunBoardFailureWatch watches for SessionEnd-style hook events from board agents and posts the stage's *-failed marker when an agent exits WITHOUT having posted its stage's done-marker. This closes the gap where an agent dies (or is killed) mid-stage: the board would otherwise show a stuck spinner forever. It mirrors RunAgentCleanup's subscribe loop.

commenterFor resolves the PM-role commenter lazily (per event) so the watcher holds no tracker handle across its lifetime; the PM commenter MUST be resolved by role, never by key prefix (both trackers may share a name).

func RunRemote

func RunRemote(addr, token string, args []string, version string) (int, error)

RunRemote connects to the daemon at addr, sends the CLI args, and returns the exit code. Stdout and stderr are written to os.Stdout and os.Stderr.

func RunRemoteCapture

func RunRemoteCapture(addr, token string, args []string) ([]byte, error)

RunRemoteCapture connects to the daemon and runs args, returning stdout as bytes instead of printing to os.Stdout.

func SendConfirmDecision

func SendConfirmDecision(addr, token, id string, approved bool) error

SendConfirmDecision sends a confirmation decision for a pending destructive operation.

func SetLogMode

func SetLogMode(addr, token, mode string) (string, error)

SetLogMode sets the traffic log mode on the daemon. Returns the new mode.

func Subscribe

func Subscribe(addr, token string) (<-chan SubscribeEvent, func(), error)

Subscribe opens a persistent connection to the daemon's subscribe endpoint. It returns a channel that receives a signal each time the daemon's state changes, and a cleanup function that closes the connection. The channel is closed when the connection drops or cleanup is called.

func TokenPath

func TokenPath() string

TokenPath returns the default path for the daemon token file.

func WriteConnected

func WriteConnected(path string, pids []int) error

WriteConnected atomically writes the connected PIDs to path. On rename failure the temporary file is removed so a crashed daemon does not leave orphan .tmp files that survive across restarts when the process is killed with SIGKILL.

func WriteInfo

func WriteInfo(info DaemonInfo) error

WriteInfo writes the daemon info as JSON to InfoPath with restricted permissions.

func WritePidFile

func WritePidFile(pid int) error

WritePidFile writes the given PID to the PID file.

Types

type AgentCleaner

type AgentCleaner interface {
	DeleteAgent(ctx context.Context, name string) error
	// DecommissionAgent removes the agent from the list immediately and
	// returns the container ID for background teardown. This makes
	// "human agent list" responsive while the slow container stop happens
	// asynchronously.
	DecommissionAgent(name string) (containerID string, err error)
	// StopContainer stops and removes a container by ID.
	StopContainer(ctx context.Context, containerID string) error
}

AgentCleaner stops and removes an agent by name.

type AgentInfo

type AgentInfo struct {
	Name        string
	ContainerID string
	CreatedAt   time.Time
}

AgentInfo holds the minimum metadata needed by the zombie sweep.

type AgentLauncher added in v0.20.0

type AgentLauncher interface {
	Launch(ctx context.Context, name, prompt, workspace, configDir string) error
}

AgentLauncher launches a containerized agent for a board stage. It is an interface so the transition engine is testable without Docker.

type AgentZombieSweeper

type AgentZombieSweeper interface {
	RunningAgents() ([]AgentInfo, error)
	IsProcessRunning(ctx context.Context, containerID string, process string) (bool, error)
	DeleteAgent(ctx context.Context, name string) error
}

AgentZombieSweeper checks for orphaned agent containers whose main process has exited but the container is still running.

type BoardCard added in v0.20.0

type BoardCard struct {
	Stage          BoardStage `json:"stage"`
	State          BoardState `json:"state"`
	EngineeringKey string     `json:"engineering_key,omitempty"`
	Branch         string     `json:"branch,omitempty"`
	PRURL          string     `json:"pr_url,omitempty"`
	Error          string     `json:"error,omitempty"`
}

BoardCard is the derived per-PM placement on the pipeline board. It is the single source of truth shared on the wire with the GUI and TUI, so neither re-derives from raw comments.

func DeriveBoardCard added in v0.20.0

func DeriveBoardCard(comments []tracker.Comment, statusType tracker.Category) BoardCard

DeriveBoardCard computes a PM ticket's board placement from its comment thread and tracker status. The rule: the furthest stage carrying ANY marker wins; within that stage the latest marker (by Created) decides running/done/failed. A ticket with no markers sits in Backlog while open and is Hidden once closed/done (those never entered the pipeline). Pure: no I/O.

type BoardStage added in v0.20.0

type BoardStage string

BoardStage is one of the five pipeline columns (plus a synthetic "hidden" stage for closed PM tickets that never entered the pipeline). The drag-board GUI renders cards into these columns; the daemon derives the stage from the [human:…] comment markers a PM ticket carries.

const (
	BoardBacklog        BoardStage = "backlog"
	BoardPlanning       BoardStage = "planning"
	BoardImplementation BoardStage = "implementation"
	BoardVerification   BoardStage = "verification"
	BoardDoneStage      BoardStage = "done"
	BoardHidden         BoardStage = "hidden"
)

type BoardState added in v0.20.0

type BoardState string

BoardState is the within-stage status of a card: empty for an idle card sitting at the head of a stage, running while an agent works the stage, done once the stage's success marker lands, failed on an error marker.

const (
	BoardIdle    BoardState = ""
	BoardRunning BoardState = "running"
	BoardDone    BoardState = "done"
	BoardFailed  BoardState = "failed"
)

type BoardTransitionDeps added in v0.20.0

type BoardTransitionDeps struct {
	Commenter    tracker.Commenter
	Launcher     AgentLauncher
	Publisher    PRPublisher
	WorkspaceDir string
	ConfigDir    string
}

BoardTransitionDeps wires the transition engine's collaborators.

func (BoardTransitionDeps) ApplyTransition added in v0.20.0

func (d BoardTransitionDeps) ApplyTransition(ctx context.Context, req BoardTransitionRequest) error

ApplyTransition advances a card from its current stage to the requested next stage. The daemon re-loads live comments and re-derives the card here because the UI gate is advisory only — the daemon is the authority on whether a forward move is allowed (forward-only, single-step, gated on the prior stage's completion). All errors carry details for the client.

type BoardTransitionRequest added in v0.20.0

type BoardTransitionRequest struct {
	PMKey   string     `json:"pm_key"`
	PMTitle string     `json:"pm_title"`
	From    BoardStage `json:"from"`
	To      BoardStage `json:"to"`
}

BoardTransitionRequest is the wire request for advancing a card one stage. PMTitle is carried from the card so the Done stage can title the PR without a second tracker fetch.

type BrowserOpener

type BrowserOpener interface {
	Open(url string) error
}

BrowserOpener opens a URL in the browser. Extracted for testability.

type ConnectedTracker

type ConnectedTracker struct {
	// contains filtered or unexported fields
}

ConnectedTracker maintains a thread-safe set of recently-seen client PIDs. Each PID has a last-seen timestamp; Prune removes entries older than a TTL.

func NewConnectedTracker

func NewConnectedTracker() *ConnectedTracker

NewConnectedTracker creates an empty tracker.

func (*ConnectedTracker) PIDs

func (t *ConnectedTracker) PIDs() []int

PIDs returns a sorted snapshot of currently tracked PIDs.

func (*ConnectedTracker) Prune

func (t *ConnectedTracker) Prune(ttl time.Duration)

Prune removes PIDs not seen within ttl.

func (*ConnectedTracker) Touch

func (t *ConnectedTracker) Touch(pid int)

Touch records or refreshes a PID with the current time.

type DaemonInfo

type DaemonInfo struct {
	Addr       string `json:"addr"`
	ChromeAddr string `json:"chrome_addr,omitempty"`
	ProxyAddr  string `json:"proxy_addr,omitempty"`
	Token      string `json:"token,omitempty"`
	PID        int    `json:"pid,omitempty"`
	// Version carries the daemon binary's build version so clients can warn
	// about skew between the running daemon and the CLI binary.
	// omitempty preserves backward-compatibility with daemon.json files
	// written by older builds that do not emit this field.
	Version  string        `json:"version,omitempty"`
	Projects []ProjectInfo `json:"projects,omitempty"`
}

DaemonInfo holds the runtime details of a running daemon instance.

func ReadInfo

func ReadInfo() (DaemonInfo, error)

ReadInfo reads and unmarshals the daemon info from InfoPath.

func (DaemonInfo) IsAlive

func (d DaemonInfo) IsAlive() bool

IsAlive checks whether the daemon process identified by PID is still running.

func (DaemonInfo) IsReachable

func (d DaemonInfo) IsReachable() bool

IsReachable checks whether the daemon is accepting TCP connections at its advertised address. This works across process namespaces (e.g. host ↔ devcontainer) where PID-based checks fail.

type HookEventStore

type HookEventStore struct {
	// contains filtered or unexported fields
}

HookEventStore is a thread-safe ring buffer of recent hook events. It stores raw events and can derive per-session snapshots on demand. Subscribers are notified (non-blocking) whenever a new event is appended.

func NewHookEventStore

func NewHookEventStore() *HookEventStore

NewHookEventStore creates an empty store.

func (*HookEventStore) Append

func (s *HookEventStore) Append(evt hookevents.Event)

Append adds a hook event. Before appending, events for the same session beyond maxHookEventsPerSession are dropped so one chatty client cannot push other sessions out of the shared buffer. The aggregate cap then drops the oldest events across all sessions.

func (*HookEventStore) EventsSince

func (s *HookEventStore) EventsSince(since uint64) ([]hookevents.Event, uint64)

EventsSince returns the events appended after the given sequence, plus the current high-water sequence to pass on the next call. Sequences are monotonic and independent of the ring's length, so a subscriber keeps receiving new events even after the ring saturates and stops growing — the failure mode of tracking deltas by slice length. Pass 0 for the first call.

func (*HookEventStore) RecentEvents

func (s *HookEventStore) RecentEvents() []hookevents.Event

RecentEvents returns a copy of all stored events.

func (*HookEventStore) Snapshot

func (s *HookEventStore) Snapshot() map[string]hookevents.SessionSnapshot

Snapshot returns the current per-session state derived from all stored events.

func (*HookEventStore) Subscribe

func (s *HookEventStore) Subscribe() chan struct{}

Subscribe returns a channel that receives a signal whenever a new event is appended. The channel has a buffer of 1 so a single pending notification is coalesced. Call Unsubscribe to clean up.

func (*HookEventStore) Unsubscribe

func (s *HookEventStore) Unsubscribe(ch chan struct{})

Unsubscribe removes a previously registered channel from the subscriber list. The channel is not closed — subscribers must stop reading from it after calling Unsubscribe and let it be garbage collected. This avoids coordinating with any concurrent Append on a removed channel.

type NetworkEvent

type NetworkEvent struct {
	Source   string    `json:"source"` // "proxy" | "oauth" | "fail"
	Status   string    `json:"status"` // "forward" | "intercept" | "block" | "no-sni" | "parse-fail" | "dial-fail" | "callback"
	Host     string    `json:"host"`   // may be empty for pre-SNI failures
	Count    int       `json:"count"`  // >=1
	LastSeen time.Time `json:"last_seen"`
}

NetworkEvent is a single ambient network activity row as rendered by the TUI activity panel. Consecutive events with the same Host and Source are collapsed into one row with an incrementing Count and a refreshed LastSeen timestamp.

func GetNetworkEvents

func GetNetworkEvents(addr, token string) ([]NetworkEvent, error)

GetNetworkEvents fetches the current ambient network activity buffer from the daemon. Returns a nil slice (not a nil error) when the daemon replies with an empty list so the TUI can collapse the panel.

type NetworkEventStore

type NetworkEventStore struct {
	// contains filtered or unexported fields
}

NetworkEventStore is a thread-safe ring buffer of recent network events with consecutive-host deduplication. It models HookEventStore but collapses bursts at write time so the panel stays calm under sustained repeats.

func NewNetworkEventStore

func NewNetworkEventStore() *NetworkEventStore

NewNetworkEventStore creates an empty store using time.Now as its clock. Tests can override the clock via NewNetworkEventStoreWithClock.

func NewNetworkEventStoreWithClock

func NewNetworkEventStoreWithClock(nowFn func() time.Time) *NetworkEventStore

NewNetworkEventStoreWithClock is the test constructor that injects a deterministic clock so dedup timestamps are reproducible.

func (*NetworkEventStore) Emit

func (s *NetworkEventStore) Emit(source, status, host string)

Emit satisfies proxy.NetworkEventEmitter. Consecutive events with the same (source, host) pair are collapsed into the tail row: Count is incremented and LastSeen is refreshed. A different host or source starts a new row, even if the host was seen earlier in the buffer. Collapsing at write time keeps memory flat under sustained bursts so the panel stays calm under noise.

func (*NetworkEventStore) Snapshot

func (s *NetworkEventStore) Snapshot() []NetworkEvent

Snapshot returns a copy of all current rows in insertion order (oldest first). Callers that want newest-first should reverse the slice; the store keeps events in insertion order so testing and JSON serialization remain predictable.

type PRPublisher added in v0.20.0

type PRPublisher interface {
	PushAndCreatePR(ctx context.Context, req PRRequest) (prURL string, err error)
}

PRPublisher pushes the recorded branch and opens a pull request. Injected so the Done stage is testable without git/forge access.

type PRRequest added in v0.20.0

type PRRequest struct {
	WorkspaceDir string
	Branch       string
	Title        string
	Body         string
}

PRRequest carries everything needed to push a branch and open its PR.

type PendingConfirm

type PendingConfirm struct {
	ID        string `json:"id"`
	Operation string `json:"operation"` // "DeleteIssue", "EditIssue"
	Tracker   string `json:"tracker"`   // tracker kind, e.g. "jira", "linear"
	Key       string `json:"key"`       // issue key, e.g. "KAN-1"
	Prompt    string `json:"prompt"`
	CreatedAt string `json:"created_at"`
	ClientPID int    `json:"client_pid"` // PID of the Claude instance that triggered the operation
}

PendingConfirm is the wire type for a single pending destructive operation awaiting user confirmation via the TUI.

func GetPendingConfirms

func GetPendingConfirms(addr, token string) ([]PendingConfirm, error)

GetPendingConfirms fetches pending destructive operation confirmations from the daemon.

type PendingConfirmStore

type PendingConfirmStore struct {
	// contains filtered or unexported fields
}

PendingConfirmStore is a thread-safe store for destructive operations awaiting user confirmation. The daemon adds entries when it intercepts destructive commands; the TUI polls the snapshot and resolves them.

func NewPendingConfirmStore

func NewPendingConfirmStore() *PendingConfirmStore

NewPendingConfirmStore creates an empty store.

func (*PendingConfirmStore) Add

Add stores a pending confirmation. The caller should block on pc.Decision after calling Add.

func (*PendingConfirmStore) Cleanup

func (s *PendingConfirmStore) Cleanup(maxAge time.Duration)

Cleanup rejects and removes all pending confirmations older than maxAge.

func (*PendingConfirmStore) Len

func (s *PendingConfirmStore) Len() int

Len returns the number of pending confirmations.

func (*PendingConfirmStore) Resolve

func (s *PendingConfirmStore) Resolve(id string, approved bool, approverPID int) error

Resolve sends the decision to the waiting goroutine and removes the entry. approverPID is the PID of the client resolving the confirmation and must be a positive integer.

The approverPID != requester PID check below is only a best-effort sanity guard, NOT an authorization boundary: ClientPID is supplied by the client and the requester's PID is typically resolved inside the agent's container namespace while the approver's is on the host, so the two are not comparable as a trust signal. Actual authorization is the daemon token required to reach this endpoint at all — do not rely on the PID check for security.

Resolve is for client-initiated decisions. Internal lifecycle events (timeouts, encode failures) must use ResolveTimeout instead.

func (*PendingConfirmStore) ResolveTimeout

func (s *PendingConfirmStore) ResolveTimeout(id string)

ResolveTimeout removes a pending confirmation without a client approver. It is used by internal lifecycle events (request timeouts, response-write failures) that need to unblock the waiting goroutine. The decision is always "not approved".

Returns nil even when the id is unknown — the caller is typically running in a deferred cleanup path where the entry may have already been resolved by another lifecycle event, and that is not a failure.

func (*PendingConfirmStore) Snapshot

func (s *PendingConfirmStore) Snapshot() []PendingConfirm

Snapshot returns all pending confirmations as wire types for the TUI.

type PendingConfirmation

type PendingConfirmation struct {
	ID        string
	Operation string // "DeleteIssue", "EditIssue"
	Tracker   string // tracker kind, e.g. "jira"
	Key       string // issue key, e.g. "KAN-1"
	Prompt    string // human-readable, e.g. "Delete KAN-1?"
	ClientPID int    // PID of the Claude instance that triggered the operation
	CreatedAt time.Time
	Decision  chan bool // the blocked goroutine waits on this; true = approved
}

PendingConfirmation represents a destructive operation that is blocked waiting for user confirmation via the TUI.

type ProjectEntry

type ProjectEntry struct {
	Name string // from .humanconfig project: field, or directory basename
	Dir  string // absolute path to project directory
}

ProjectEntry holds the loaded config context for one registered project directory.

func (ProjectEntry) EnvLookup

func (p ProjectEntry) EnvLookup() config.EnvLookup

EnvLookup returns a per-project scoped environment variable lookup function. It implements a 4-level precedence chain for each key:

  1. HUMAN_{PROJECT}_{KEY} — per-project override (e.g. HUMAN_INFRA_GITHUB_WORK_TOKEN)
  2. {KEY} via os.LookupEnv — global fallback (e.g. GITHUB_WORK_TOKEN)

The caller (ApplyEnvOverrides) constructs keys like PREFIX_SUFFIX and PREFIX_INSTANCE_SUFFIX. This lookup prepends HUMAN_{PROJECT}_ and checks that first, falling back to os.LookupEnv for the original key.

type ProjectInfo

type ProjectInfo struct {
	Name string `json:"name"`
	Dir  string `json:"dir"`
}

ProjectInfo describes a registered project in a running daemon.

type ProjectRegistry

type ProjectRegistry struct {
	// contains filtered or unexported fields
}

ProjectRegistry maps project directories to their config entries. It is created at daemon startup and is read-only thereafter (no mutex needed).

func NewProjectRegistry

func NewProjectRegistry(dirs []string) (*ProjectRegistry, error)

NewProjectRegistry creates a registry from a list of project directories. Each directory must exist and contain a readable .humanconfig. If .humanconfig lacks a project: field, the directory basename is used as the name.

func (*ProjectRegistry) Entries

func (r *ProjectRegistry) Entries() []ProjectEntry

Entries returns all registered project entries.

func (*ProjectRegistry) Resolve

func (r *ProjectRegistry) Resolve(cwd string) (ProjectEntry, bool)

Resolve finds the ProjectEntry whose Dir is a prefix of the given cwd. Returns (entry, true) on match, (zero, false) if no match. When multiple entries match (nested dirs), the longest prefix wins because entries are sorted by path length descending.

func (*ProjectRegistry) Single

func (r *ProjectRegistry) Single() bool

Single returns true if there is exactly one registered project (backward compat mode).

type Request

type Request struct {
	Version   string            `json:"version"`
	Token     string            `json:"token"`
	Args      []string          `json:"args"`
	Env       map[string]string `json:"env,omitempty"`
	ClientPID int               `json:"client_pid,omitempty"` // parent PID (Claude process) for connection tracking
	Cwd       string            `json:"cwd,omitempty"`        // client working directory for project routing
}

Request is sent from the client to the daemon (one JSON line per connection).

type Response

type Response struct {
	Stdout        string `json:"stdout"`
	Stderr        string `json:"stderr"`
	ExitCode      int    `json:"exit_code"`
	AwaitCallback bool   `json:"await_callback,omitempty"`
	Callback      string `json:"callback,omitempty"`
	AwaitConfirm  bool   `json:"await_confirm,omitempty"`  // line 1: daemon paused, awaiting TUI confirmation
	ConfirmID     string `json:"confirm_id,omitempty"`     // unique identifier for the pending operation
	ConfirmPrompt string `json:"confirm_prompt,omitempty"` // human-readable prompt, e.g. "Delete JIRA-123?"
}

Response is sent from the daemon back to the client (one or more JSON lines per connection).

type Server

type Server struct {
	Addr             string
	Token            string
	SafeMode         bool
	CmdFactory       func() *cobra.Command
	Opener           BrowserOpener // used for OAuth relay; defaults to browser.DefaultOpener
	Logger           zerolog.Logger
	ConnectedPIDs    *ConnectedTracker                        // tracks client PIDs that have pinged; nil disables tracking
	HookEvents       *HookEventStore                          // in-memory hook event buffer; nil disables hook event tracking
	NetworkEvents    *NetworkEventStore                       // in-memory ambient network activity buffer; nil disables
	IssueFetcher     func() ([]TrackerIssuesResult, error)    // injected; fetches issues from configured trackers
	TrackerDiagnoser func(dir string) []tracker.TrackerStatus // injected; diagnoses tracker status with vault resolution
	Projects         *ProjectRegistry                         // multi-project routing; nil means single-project mode
	PendingConfirms  *PendingConfirmStore                     // pending destructive operation confirmations; nil disables
	StatsWriter      *stats.Writer                            // async SQLite writer for tool event persistence; nil disables
	StatsStore       *stats.StatsStore                        // for query-time aggregation; nil disables tool-stats route
	AuditSink        *audit.Writer                            // records mutating tracker actions for the audit trail; nil disables
	AuditStore       *audit.Store                             // serves audit-query reads; nil disables audit-query route
	AgentCleaner     AgentCleaner                             // async agent cleanup; nil disables agent-stop-async route
	VaultResolver    *vault.Resolver                          // session-scoped vault resolver; reused across requests to avoid repeated op.exe calls
	// BoardTransitioner applies a board-transition request (advancing a card
	// one pipeline stage). nil disables the board-transition route.
	BoardTransitioner func(req BoardTransitionRequest) error
	// contains filtered or unexported fields
}

Server listens for incoming client connections and executes CLI commands.

func (*Server) ListenAndServe

func (s *Server) ListenAndServe(ctx context.Context) error

ListenAndServe starts the TCP listener and blocks until ctx is cancelled. On shutdown it waits for all in-flight handler goroutines to return before closing, so a client request that's already accepted is never torn down mid-flight by listener close alone.

type SubscribeEvent

type SubscribeEvent struct {
	Type      string `json:"type"`            // "change", "agent-stopped"
	AgentName string `json:"agent,omitempty"` // set for agent lifecycle events
}

SubscribeEvent is a notification sent over a persistent subscribe connection. For "agent-stopped" events, AgentName identifies the agent to remove immediately without waiting for the next discovery cycle.

type TrackerIssuesResult

type TrackerIssuesResult struct {
	TrackerName    string          `json:"tracker_name"`
	TrackerKind    string          `json:"tracker_kind"`
	TrackerRole    string          `json:"tracker_role,omitempty"`
	Project        string          `json:"project"`
	Issues         []tracker.Issue `json:"issues"`
	ReadyForReview []string        `json:"ready_for_review,omitempty"`
	// ReadyForReviewPRs maps an engineering ticket key to the pull-request URL
	// carried on its handoff comment's optional `pr:` line, when present.
	ReadyForReviewPRs map[string]string `json:"ready_for_review_prs,omitempty"`
	// BoardCards is the derived pipeline placement per PM issue key, for the
	// drag-board GUI. It is PM-role-only (maps a PM issue key → its derived
	// BoardCard) and is left nil on engineering-tracker results.
	BoardCards map[string]BoardCard `json:"board_cards,omitempty"`
	Err        string               `json:"error,omitempty"`
}

TrackerIssuesResult is the wire type for a single tracker/project's issues.

ReadyForReview carries the engineering ticket keys that a PM tracker has currently flagged for review via a [human:ready-for-review] comment. It is populated on engineering-tracker results (where the keys actually live) so the TUI can join it against Issues without a separate lookup. See cli/CLAUDE.md "Review handoff" for the comment convention.

func GetTrackerIssues

func GetTrackerIssues(addr, token string) ([]TrackerIssuesResult, error)

GetTrackerIssues fetches open issues from all configured tracker projects via the daemon.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL