handoff

package
v0.6.0 Latest Latest
Warning

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

Go to latest
Published: Aug 13, 2026 License: MIT Imports: 18 Imported by: 0

Documentation

Overview

Package handoff assembles a redacted, provenance-rich markdown bundle from an active Semantica capture session so a fresh agent session can pick up where the previous one left off without re-reading the original transcript.

The service is the source of truth for handoff content. Both the terminal-facing `semantica handoff --write` command and the hidden `semantica skills handoff` backing command call into this package so the bundle shape is identical across surfaces.

Index

Constants

View Source
const HandoffFilename = "handoff.md"

HandoffFilename is the relative path inside the repo's `.semantica/` directory that the writer targets.

Variables

View Source
var ErrAmbiguousSession = errors.New("multiple agent sessions active for this repo")

ErrAmbiguousSession is returned when more than one distinct provider has active capture state in the repo. Multiple sessions for the same provider auto-resolve through that provider's latest lineage row; distinct providers require a caller choice. Callers can use errors.As to unwrap an AmbiguousActiveSessionError.

View Source
var ErrAutoSelectFailed = errors.New("could not resolve auto-selected provider")

ErrAutoSelectFailed is the auto-collapse sibling of ErrNoFromMatch. When multiple capture states for one provider are active and the service silently routes through the from path for that provider, a downstream lineage miss surfaces as this error, not ErrNoFromMatch. Wraps the same specific reasons ("no recent X session", "lineage.db not found", etc.); only the surface shaping differs at the command layer.

View Source
var ErrBundleMissing = errors.New("handoff bundle not found")

ErrBundleMissing indicates `.semantica/handoff.md` does not exist for the current repo. The continue command surfaces this by pointing the user at `semantica handoff --write` first.

View Source
var ErrNoFromMatch = errors.New("could not resolve --from source")

ErrNoFromMatch is returned when an explicit --from source cannot be resolved. The wrapped error includes the specific reason, such as missing lineage data or no recent session for that provider.

Only used when the user typed --from. When the service auto-routes through the from path (same-provider collapse), failures wrap ErrAutoSelectFailed instead so the command layer does not advise the user to "drop --from" they never typed.

View Source
var ErrNoSession = errors.New("no agent session found for this repo")

ErrNoSession is returned when no usable Semantica capture session resolves for the current repo. Callers translate this into a non-zero exit with a clear user message.

View Source
var ErrUnknownProvider = errors.New("unknown agent provider")

ErrUnknownProvider indicates --agent named a provider we don't have any launch knowledge for. Distinct from "no spawn for this provider" (which is the LaunchSpec.Spawn=false path): unknown provider means we don't even have a print-the-command fallback because we don't know which binary to suggest.

Functions

func ContinuePromptFor

func ContinuePromptFor(bundlePath string) string

ContinuePromptFor returns the starter prompt the launcher passes to the spawned agent for a given bundle path. The path is embedded verbatim so the spawned agent's cwd doesn't matter and the print-path command stays copy-pasteable from any terminal regardless of the user's current working directory. Callers always pass an absolute path; relative paths break the "run this from a fresh terminal" promise.

func LookPathForTest

func LookPathForTest() func(string) (string, error)

LookPathForTest returns the package-level lookPath stub. Used by command-level tests in another package that need to swap in a deterministic binary detector. Not part of the public API; only intended for tests.

func ProviderFromBundle

func ProviderFromBundle(body []byte) string

ProviderFromBundle extracts the provider name from the "Original session" line emitted by renderBundle. Returns an empty string when the line is absent or malformed; callers surface that as "couldn't determine which agent; pass --agent."

func SetLookPathForTest

func SetLookPathForTest(fn func(string) (string, error))

SetLookPathForTest replaces the package-level lookPath stub. Pair with LookPathForTest to capture the original and restore in t.Cleanup.

Types

type ActiveProvider

type ActiveProvider struct {
	// Provider is the hook-form provider name (claude-code,
	// gemini-cli, etc.) the command layer surfaces to the user and
	// passes back as --from.
	Provider string

	// Count is the number of active capture states for this
	// provider. Surfaced in the picker label so the user can spot
	// stale-orphan clusters at a glance.
	Count int

	// LatestTimestamp is the most-recent capture-state timestamp
	// among this provider's active states. Used to sort the picker
	// (most-recent first) and to render the "latest Xm ago" hint.
	LatestTimestamp time.Time
}

ActiveProvider describes one distinct provider with active capture states in the repo. Used by the ambiguity resolution flow to populate the picker (interactive) or the error message (non-interactive).

type AmbiguousActiveSessionError

type AmbiguousActiveSessionError struct {
	Providers []ActiveProvider
}

AmbiguousActiveSessionError carries the candidate provider list alongside the ErrAmbiguousSession sentinel so the command layer can either show a picker (TTY) or print the list in a clear non-interactive error. Implements Is(target) so existing errors.Is(err, ErrAmbiguousSession) checks keep working.

func (*AmbiguousActiveSessionError) Error

func (*AmbiguousActiveSessionError) Is

func (e *AmbiguousActiveSessionError) Is(target error) bool

Is wires errors.Is(err, ErrAmbiguousSession) back to true so existing call sites that test the sentinel keep working without knowing about the typed wrapper.

type Input

type Input struct {
	// RepoPath is the working repository whose session is being
	// handed off. Defaults to the current working directory at the
	// command layer.
	RepoPath string

	// Now is the wall-clock used for "is this session recent enough"
	// checks. Tests inject a fixed time. Empty value means time.Now().
	Now time.Time

	// From, when non-empty, sources the bundle from the named
	// provider's most-recent session in this repo. The value is the
	// hook-form provider name (claude-code, cursor, gemini-cli,
	// copilot, kiro-cli, kiro-ide). Empty uses the default resolution
	// chain: active capture state, then lineage fallback.
	From string
}

Input narrows the surface the caller has to provide. RepoPath is the only required field; the service derives everything else from the repo's lineage.db and the global capture-state directory.

type LaunchSpec

type LaunchSpec struct {
	// Provider is the canonical name of the agent (matches the
	// capture-state and SKILL.md naming, e.g. "claude-code").
	Provider string

	// Binary is the executable name on PATH (e.g. "claude" for
	// claude-code). Empty when Spawn is false and the user must
	// run the agent manually.
	Binary string

	// Args are the positional + flag arguments to pass to Binary.
	// The starter prompt is included here in whatever shape the
	// target agent's CLI accepts.
	Args []string

	// Spawn reports whether this launch can be exec'd directly.
	// When false, callers print Message and exit; the user is
	// expected to invoke the agent themselves.
	Spawn bool

	// Message is the human-readable text the command layer prints
	// before either spawning (informational) or surrendering to
	// the manual flow (the explanation of why we can't spawn).
	Message string
}

LaunchSpec describes how the continue command should hand off to the next agent. The command layer turns this into either an actual exec (when Spawn is true) or a printed instruction the user runs themselves (when Spawn is false).

func BuildLaunchSpec

func BuildLaunchSpec(provider, bundlePath string, printOnly bool) (*LaunchSpec, error)

BuildLaunchSpec builds launch configuration for the given provider. The returned spec carries enough information for the command layer to either exec the agent's binary (Spawn=true) or print a manual-launch hint (Spawn=false). printOnly forces the print-the-command branch even for providers we know how to spawn, which lets users copy the invocation rather than land in a new agent shell.

bundlePath must be absolute. The starter prompt and any manual- launch hint embed it directly so the resulting commands are usable from any directory the user might paste them into.

type Result

type Result struct {
	// Path is the absolute filesystem path of the written bundle.
	Path string

	// SessionID is the resolved session whose context the bundle
	// captures. Surfaced for diagnostics and tests.
	SessionID string

	// Provider is the capture provider for the resolved session.
	Provider string

	// Bytes is the raw markdown body that was written. Returned for
	// tests; do not echo this back into the originating session.
	Bytes []byte
}

Result describes what was written so the caller can render its two-line user instruction.

type Service

type Service struct{}

Service assembles handoff bundles. Construct via NewService.

func NewService

func NewService() *Service

NewService returns a stateless service. All dependencies (lineage store, redactor, capture-state directory) are reached through existing internal packages.

func (*Service) Write

func (s *Service) Write(ctx context.Context, in Input) (*Result, error)

Write resolves the active session for the repo, assembles a redacted markdown bundle, and writes it to `<repo>/.semantica/handoff.md`. Returns the resolved session and the bytes written.

Session resolution has three layers, in priority order:

  1. Explicit --from: when Input.From names a provider, source the bundle from that provider's most-recent session in the repo regardless of which agent currently holds the active capture state.
  2. Active capture state, written by the agent's prompt-submit hook and deleted by the stop hook at end of turn. Works in-turn (e.g., from the skill body's bash invocation while the agent is still mid-response).
  3. Lineage fallback: when no active capture state matches, look up the most-recent parent session for the repo in agent_sessions (within the same 24h recency window) that has at least one event. This is what makes `handoff --write` work between turns, when capture state has been cleaned up but durable lineage data still exists.

Jump to

Keyboard shortcuts

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