Documentation
¶
Overview ¶
SessionStart hook logic for `wbt context session-start` (formerly the standalone wbt-context binary). Emits a JSON object with a "systemMessage" field that Claude Code injects into the first user message of a new session.
Exit semantics: this function returns nil unconditionally so the wbt dispatcher (and the wbt-context shim) always exit 0 — Claude Code MUST never be blocked by a hook error. All failures are logged via slog to a 0600 file in os.TempDir.
Stop hook + health snapshot logic for `wbt doctor` (formerly the standalone wbt-doctor binary). Connects to the wayneblacktea Postgres database and emits a JSON snapshot of personal-OS health (stuck in-progress tasks, pending proposal queue depth, due reviews) plus a list of human-readable "forgotten signals" — short strings flagging likely Claude omissions.
Designed to run as a Claude Code Stop hook so the next SessionStart can surface the previous session's open loops without depending on the live MCP process (which is gone by Stop time).
Stop hook upgrade (session lifecycle): if stdin contains a Claude Code transcript JSON (non-empty), the doctor:
- Calls Haiku to produce a ≤500-char plain-text summary.
- Writes the summary to session_handoffs.summary_text (best-effort).
- Saves the summary as a zettelkasten knowledge_item (type=zettelkasten, source=auto-summary) for long-term searchable recall.
Output:
- JSON to stdout (parseable by SessionStart hook / claude-hud)
- Forgotten signals also written to stderr in human-readable form
- Always returns nil so wbt (and the wbt-doctor shim) exits 0
Package cli contains the logic for the wbt CLI subcommands. It is extracted from cmd/wbt/main.go so that the pure helper functions can be unit-tested without subprocess execution or file I/O.
PostToolUse hook logic for `wbt hook` (formerly the standalone wbt-hook binary).
Claude Code calls this subcommand after every tool execution (Bash, Edit, Write, Read, MCP, etc.) with a JSON payload on stdin.
Spec (Claude Code hooks):
stdin — JSON: {"tool_name":..., "tool_input":..., "tool_response":{"text":...},
"tool_use_id":..., "cwd":..., "session_id":..., "transcript_path":...}
stdout — optional JSON: {"additionalContext": "..."} (≤ 10 000 chars)
exit 0 — always; hook MUST NOT block the Claude Code session
Safety constraints:
- Read at most 300 bytes from stdin (claude-mem bug #1220 workaround)
- Total execution time budget: 50 ms (enqueue only, no DB / LLM wait)
- POST to wayneblacktea server with 200 ms timeout
- Always returns nil; errors are slog'd, never propagated
reembed_cmd.go — wbt reembed subcommand
Idempotent backfill command that re-embeds historical rows in session_handoffs and decisions whose embedding is NULL or whose embedding_provider does not match the currently-configured provider.
Usage:
wbt reembed [--table session_handoffs|decisions|all] [--batch N] [--dry-run]
Design notes:
- Resumable: uses "WHERE embedding IS NULL OR embedding_provider <> $target" with ORDER BY created_at so interrupted runs restart cleanly.
- Rate-limited: configurable batch size + per-batch sleep to avoid 429s.
- Fail-soft: a per-row embed or DB error is logged and skipped; the loop continues to the next row.
- NOT run automatically: the operator invokes this manually against prod Aiven AFTER setting GEMINI_API_KEY and DATABASE_URL in the environment.
- This file DOES NOT connect to any live DB during build/task check — all DB I/O is behind a --dry-run guard that bypasses real connections.
Index ¶
- Constants
- func BuildHookNotes(toolInput string) string
- func CollectAPIKey(r *bufio.Reader) (string, error)
- func ConfigPath() (string, error)
- func DSNFromFallback() string
- func DetectDoctorSignals(s DoctorSnapshot) []string
- func EmitContext(msg string)
- func GenerateEnvFile(apiKey, port string, db DBConfig) string
- func HealthOK(port int) bool
- func HomeDir() string
- func InitHookSlog(name string)
- func LoadGlobalConfig() error
- func OpenBrowser(url string)
- func ParseDuration(s string) (time.Duration, error)
- func PrintHookSnippet(apiKey, port string)
- func Prompt(r *bufio.Reader, question string) (string, error)
- func PromptRequired(r *bufio.Reader, question, emptyErrMsg string) (string, error)
- func PromptWithDefault(r *bufio.Reader, question, defaultVal string) (string, error)
- func RandomHex(n int) (string, error)
- func ReadEnvPort() string
- func RegisterClaudeMCP(name, urlOrEmpty string, transport MCPTransport) error
- func ResolveAllowedOrigins(allowedOrigins, appEnv, port string) (string, error)
- func RunContext(args []string) error
- func RunDoctor(_ []string) error
- func RunGuard(args []string) error
- func RunHook(_ []string) error
- func RunInit() error
- func RunInstallGitHook(args []string) error
- func RunMCP() error
- func RunPostMergeLocal(args []string) error
- func RunReconcile(args []string) error
- func RunReembed(args []string) error
- func RunRestart(args []string) error
- func RunServe(args []string) error
- func RunSetup(args []string) error
- func RunStatus(args []string) error
- func RunStop(args []string) error
- func SetIfAbsent(key, value string)
- func SortBySimDesc[T any](s []T, score func(T) float64)
- func ValidateGuardBypassFlags(scope, target, reason string, iUnderstandGlobal bool) error
- func WorkspaceFromEnv() *uuid.UUID
- func WriteGlobalConfig(cfg WbtConfig) error
- func WriteMCPJSON(db DBConfig) ([]byte, error)
- type DBConfig
- type DoctorSnapshot
- type MCPConfig
- type MCPServer
- type MCPTransport
- type SessionStartOutput
- type StatusReport
- type WbtConfig
Constants ¶
const HookMaxStdinBytes = hookMaxStdinBytes
HookMaxStdinBytes is exported for test access — verifies the truncation cap invariant from the legacy cmd/wbt-hook test suite.
Variables ¶
This section is empty.
Functions ¶
func BuildHookNotes ¶
BuildHookNotes returns a SHA256 hex hash of toolInput, or a length-capped raw input when WBT_HOOK_RAW=1 is set (only for trusted dev environments). Exported so the hook_cmd_test.go file in the same package can exercise it (matches the legacy cmd/wbt-hook test contract).
SECURITY: raw mode caps at hookRawNotesMaxLen (500 chars) so file contents written by Edit/Write/Bash that include secrets cannot be harvested wholesale. (security audit M-3)
func CollectAPIKey ¶
CollectAPIKey asks for an API key or generates one if the user leaves it empty.
func ConfigPath ¶
ConfigPath returns the canonical path to the global config file: $XDG_CONFIG_HOME/wayneblacktea/config.yaml (defaults to ~/.config/…). The parent directory is created if it does not exist.
func DSNFromFallback ¶
func DSNFromFallback() string
DSNFromFallback reads DATABASE_URL from one of the user-config fallback locations:
- ~/.wayneblacktea/.env.local
- ~/.wayneblacktea/.env
Returns "" when no file is found or no DATABASE_URL line is present.
func DetectDoctorSignals ¶
func DetectDoctorSignals(s DoctorSnapshot) []string
DetectDoctorSignals returns the forgotten-signal strings for snapshot s. Exported for test access.
func EmitContext ¶
func EmitContext(msg string)
EmitContext writes the SessionStart hook JSON envelope to stdout. Exported for test access.
func GenerateEnvFile ¶
GenerateEnvFile returns the contents of the .env file produced by wbt init.
The output contains:
- API_KEY, PORT, STORAGE_BACKEND (always present)
- DATABASE_URL (postgres backend only)
- SQLITE_PATH (sqlite backend only)
- ALLOWED_ORIGINS defaulting to localhost on the given port
- Commented hints for optional AI provider keys
func HealthOK ¶
HealthOK is the exported probeHealth helper for tests. It performs one GET /health and returns true when the response is 200. Not intended for production callers; setup.go uses probeHealth directly.
func HomeDir ¶
func HomeDir() string
HomeDir returns the user home directory, falling back to "." on error.
func InitHookSlog ¶
func InitHookSlog(name string)
InitHookSlog redirects slog to a 0600 file in os.TempDir so a hook never writes to stderr (Claude Code surfaces stderr as terminal warnings). Falls back to io.Discard if the log file cannot be opened. The name parameter is the basename of the hook (e.g. "wbt-context", "wbt-hook", "wbt-doctor") and becomes the prefix of the log file under os.TempDir.
Shared by the wbt context|hook|doctor subcommands (formerly the standalone wbt-context / wbt-hook / wbt-doctor binaries, now thin shims that exec `wbt <subcmd>`).
func LoadGlobalConfig ¶
func LoadGlobalConfig() error
LoadGlobalConfig reads ~/.config/wayneblacktea/config.yaml and sets env vars for any keys present in the file that are not already set.
Missing file is silently ignored. Parse errors are returned.
func OpenBrowser ¶
func OpenBrowser(url string)
OpenBrowser opens url in the system default browser. Best-effort: errors are silently swallowed because a browser launch failure (headless env, no default browser configured) must not abort the server start flow.
func ParseDuration ¶
ParseDuration extends time.ParseDuration to accept "d" for days.
func PrintHookSnippet ¶
func PrintHookSnippet(apiKey, port string)
PrintHookSnippet prints a copy-pasteable ~/.claude/settings.json snippet that registers wbt-hook as a Claude Code PostToolUse global hook.
func PromptRequired ¶
PromptRequired calls Prompt and returns an error if the result is empty.
func PromptWithDefault ¶
PromptWithDefault calls Prompt and returns defaultVal if the user input is empty.
func ReadEnvPort ¶
func ReadEnvPort() string
ReadEnvPort reads the PORT env var and returns "8420" as the default.
func RegisterClaudeMCP ¶
func RegisterClaudeMCP(name, urlOrEmpty string, transport MCPTransport) error
RegisterClaudeMCP runs `claude mcp remove <name>` (best-effort) then `claude mcp add ...` to register the wayneblacktea MCP server with the user's Claude Code installation at user-global scope (default; no --scope project).
transport=http URL is the http endpoint (e.g. http://localhost:8420/mcp). transport=stdio invokes `wbt mcp`.
If the `claude` binary is not in PATH the function prints copy-paste instructions to stdout and returns nil — registration is a convenience, not a hard requirement.
The CLAUDE_BIN env var overrides the binary location for tests only.
func ResolveAllowedOrigins ¶
ResolveAllowedOrigins determines the CORS allowed-origins string for the .env file. This is the wbt-init variant (reads env vars directly so it can be tested without subprocess wiring).
Rules (mirrors cmd/server resolveAllowedOrigins):
- ALLOWED_ORIGINS="*" is always rejected.
- ALLOWED_ORIGINS non-empty → returned as-is.
- ALLOWED_ORIGINS empty + APP_ENV="production" → error.
- ALLOWED_ORIGINS empty + any other APP_ENV → default to localhost on port.
func RunContext ¶
RunContext dispatches `wbt context <subcmd>`. args is os.Args[2:] (subcmd onward). Currently supports only `session-start`. Returns nil so wbt always exits 0 — Claude Code MUST never be blocked by a hook error.
func RunDoctor ¶
RunDoctor dispatches `wbt doctor`. args is unused but kept for the standard Run<X>(args []string) error signature. Always returns nil so wbt (and the wbt-doctor shim) exits 0 — the Stop hook MUST never block Claude Code.
func RunHook ¶
RunHook dispatches `wbt hook` (PostToolUse). args is unused but kept for the standard Run<X>(args []string) error signature. Always returns nil so the wbt dispatcher (and wbt-hook shim) exits 0 — Claude Code MUST never be blocked.
func RunInit ¶
func RunInit() error
RunInit runs the interactive wizard and writes .env + .mcp.json.
func RunInstallGitHook ¶
RunInstallGitHook implements `wbt install-git-hook`.
func RunMCP ¶
func RunMCP() error
RunMCP serves MCP stdio by delegating to the shared mcprunner package (also used by cmd/mcp). Reads .env from CWD if present so users do not need to set DATABASE_URL / CLAUDE_API_KEY in the environment that Claude Code launches the hook from.
func RunPostMergeLocal ¶
RunPostMergeLocal implements `wbt post-merge-local`. It is designed to run from a git post-merge hook in an arbitrary repo and is ALWAYS fail-open: it returns nil on every non-flag error so the hook never aborts a merge.
func RunReconcile ¶
RunReconcile implements the `wbt reconcile` subcommand. args is everything after "wbt reconcile" on the command line. Returns nil on success or a fatal error; gh-missing / gh-unauth is a warning, not an error.
func RunReembed ¶
RunReembed dispatches `wbt reembed`. args is os.Args[2:]. Always returns nil so wbt exits 0 on dry-run; propagates errors otherwise.
func RunRestart ¶
RunRestart is stop + setup. We do not short-circuit on stop errors so that "stop failed because already stopped" still triggers a fresh setup attempt.
func RunServe ¶
RunServe loads config and runs wayneblacktea-server. Config is loaded in priority order (later source wins unless env var already set):
- ~/.config/wayneblacktea/config.yaml (global, written by `wbt init`)
- .env in CWD (legacy / per-project override)
- Existing environment variables (Railway, CI, etc.)
args is the slice of arguments after "serve" (i.e. os.Args[2:]).
Flags:
--no-browser suppress automatic browser launch (also suppressed by WBT_NO_BROWSER=1)
func RunSetup ¶
RunSetup orchestrates the one-command install. See package doc for the step ordering. args is the slice of CLI arguments after "setup".
Supported flags:
--port=<n> override port (default: WBT_PORT env or 8420 from config) --server-bin=<path> override wayneblacktea-server path (for tests) --mcp-name=<n> override MCP server name (default: wayneblacktea) --no-mcp skip claude mcp registration entirely
func RunStatus ¶
RunStatus implements `wbt status`. It reads the PID file, checks liveness, probes /health on the configured port, and prints the result.
Flags:
--format plain (default) one-line human summary --format json JSON shape per StatusReport
Exit code: 0 when the server is healthy; 1 when not running OR PID file references a dead process; 2 on probe failure (port unreachable but PID alive — caller may want to inspect logs).
func RunStop ¶
RunStop terminates the background wayneblacktea server identified by the PID file and removes the PID file on success. The operation is idempotent: missing PID file is reported as "already stopped" with exit code 0.
args is reserved for future flags (--force, --timeout) and is currently validated to be empty.
func SetIfAbsent ¶
func SetIfAbsent(key, value string)
SetIfAbsent sets the env var key to value only when value is non-empty and the env var is not already set.
func SortBySimDesc ¶
SortBySimDesc sorts a slice of any type by descending similarity score. Uses a simple insertion-sort-style swap (table sizes <= 200, acceptable). Exported for test access.
func ValidateGuardBypassFlags ¶
ValidateGuardBypassFlags exhaustively validates the (scope, target, reason) triple plus the global confirmation.
Validation rules:
- scope MUST be in {file, dir, repo, global}.
- When scope=global, target MUST be the literal "global" AND the --i-understand-this-is-global flag MUST be set.
- When scope in {file, dir}, target MUST be an absolute path AND MUST NOT be one of the overly-broad system roots ("/", "/home", "/Users").
- reason MUST be non-empty (non-whitespace).
func WorkspaceFromEnv ¶
WorkspaceFromEnv reads WORKSPACE_ID from the environment and returns a *uuid.UUID, or nil if the env var is missing/invalid. Used by the context and doctor hooks to scope DB queries.
func WriteGlobalConfig ¶
WriteGlobalConfig serialises cfg to ~/.config/wayneblacktea/config.yaml with mode 0600 (credential-bearing file).
func WriteMCPJSON ¶
WriteMCPJSON marshals the .mcp.json content and returns the bytes.
The MCP entry points at `wbt mcp` rather than the standalone `wayneblacktea-mcp` binary so that `go install .../cmd/wbt@latest` installs everything an end user needs.
Types ¶
type DBConfig ¶
DBConfig holds the database configuration collected during init.
func CollectDBConfig ¶
CollectDBConfig asks the user whether to use SQLite or Postgres.
func CollectPostgresConfig ¶
CollectPostgresConfig collects Postgres connection details.
type DoctorSnapshot ¶
type DoctorSnapshot struct {
GeneratedAt time.Time `json:"generated_at"`
Workspace string `json:"workspace,omitempty"`
StuckTasks []string `json:"stuck_task_ids,omitempty"`
StuckCount int `json:"stuck_count"`
InProgressCount int `json:"in_progress_count"`
PendingProposals int `json:"pending_proposals"`
DueReviews int `json:"due_reviews"`
ForgottenSignals []string `json:"forgotten_signals,omitempty"`
SessionSummary string `json:"session_summary,omitempty"`
// GoalsDue and TopPending are the OPS-1 delivery-visibility fields,
// consumed by _project/.claude/hooks/session-start.sh. Both are
// deliberately NOT `omitempty`: GoalsDue MUST always render as "[]"
// (never omitted/null) and TopPending MUST always render as an object
// or explicit "null" — see newDoctorSnapshot for the always-non-nil
// GoalsDue initialisation that makes this hold even on fail-soft paths
// that never reach the DB. Title + deadline only; no goal/task IDs,
// descriptions, or other context is exposed (backend-security-design.md
// §3.2 data minimisation).
GoalsDue []gtd.DeliveryGoal `json:"goals_due"`
TopPending *gtd.DeliveryTask `json:"top_pending"`
}
DoctorSnapshot is the JSON envelope written to stdout by `wbt doctor`. Exported so tests can construct fixtures without leaving the package.
type MCPServer ¶
type MCPServer struct {
Command string `json:"command"`
Args []string `json:"args,omitempty"`
Env map[string]string `json:"env"`
}
MCPServer represents a single MCP server entry in .mcp.json.
type MCPTransport ¶
type MCPTransport string
MCPTransport names the transport claude uses to reach wayneblacktea.
const ( // MCPTransportHTTP registers an HTTP endpoint (preferred for `wbt setup` // since the server runs in the background; one process serves many // Claude sessions). MCPTransportHTTP MCPTransport = "http" // MCPTransportStdio registers a fork-on-each-launch stdio binary. // Retained for backward compatibility with `wbt init`. MCPTransportStdio MCPTransport = "stdio" )
type SessionStartOutput ¶
type SessionStartOutput struct {
SystemMessage string `json:"systemMessage"`
}
SessionStartOutput matches the Claude Code hook spec: a JSON object with a "systemMessage" string that is prepended to the first user message.
type StatusReport ¶
type StatusReport struct {
PID int `json:"pid"`
Port int `json:"port"`
Transport string `json:"transport"`
Healthy bool `json:"healthy"`
PIDFile string `json:"pid_file"`
StartedAt *time.Time `json:"started_at,omitempty"`
Version string `json:"version,omitempty"`
}
StatusReport is the JSON shape printed by `wbt status --format json`. StartedAt is a pointer so json:"omitempty" actually drops the field when we don't know the start time (PID-file-only path).
type WbtConfig ¶
type WbtConfig struct {
APIKey string `yaml:"api_key"`
DatabaseURL string `yaml:"database_url"`
Port string `yaml:"port"`
// storage
StorageBackend string `yaml:"storage_backend"`
SQLitePath string `yaml:"sqlite_path"`
}
WbtConfig is the schema for ~/.config/wayneblacktea/config.yaml. All fields are optional strings so that partial configs are accepted on read without overriding env vars that are already set.
func DBConfigToWbtConfig ¶
DBConfigToWbtConfig converts wizard-collected DBConfig into WbtConfig.