/**
 * Extension system types.
 *
 * Extensions are TypeScript modules that can:
 * - Subscribe to agent lifecycle events
 * - Register LLM-callable tools
 * - Register commands, keyboard shortcuts, and CLI flags
 * - Interact with the user via UI primitives
 */
import type { AgentMessage, AgentToolResult, AgentToolUpdateCallback, ThinkingLevel, ToolExecutionMode } from "@mariozechner/pi-agent-core";
import type { Api, AssistantMessageEvent, AssistantMessageEventStream, Context, ImageContent, Model, OAuthCredentials, OAuthLoginCallbacks, SimpleStreamOptions, TextContent, ToolResultMessage } from "@mariozechner/pi-ai";
import type { AutocompleteItem, AutocompleteProvider, Component, EditorComponent, EditorTheme, KeyId, OverlayHandle, OverlayOptions, TUI } from "@mariozechner/pi-tui";
import type { Static, TSchema } from "typebox";
import type { Theme } from "../../modes/interactive/theme/theme.js";
import type { BashResult } from "../bash-executor.js";
import type { CompactionPreparation, CompactionResult } from "../compaction/index.js";
import type { EventBus } from "../event-bus.js";
import type { ExecOptions, ExecResult } from "../exec.js";
import type { ReadonlyFooterDataProvider } from "../footer-data-provider.js";
import type { KeybindingsManager } from "../keybindings.js";
import type { CustomMessage } from "../messages.js";
import type { ModelRegistry } from "../model-registry.js";
import type { BranchSummaryEntry, CompactionEntry, ReadonlySessionManager, SessionEntry, SessionManager } from "../session-manager.js";
import type { SlashCommandInfo } from "../slash-commands.js";
import type { SourceInfo } from "../source-info.js";
import type { BuildSystemPromptOptions } from "../system-prompt.js";
import type { BashOperations } from "../tools/bash.js";
import type { EditToolDetails } from "../tools/edit.js";
import type { BashToolDetails, BashToolInput, EditToolInput, FindToolDetails, FindToolInput, GrepToolDetails, GrepToolInput, LsToolDetails, LsToolInput, ReadToolDetails, ReadToolInput, WriteToolInput } from "../tools/index.js";
export type { ExecOptions, ExecResult } from "../exec.js";
export type { BuildSystemPromptOptions } from "../system-prompt.js";
export type { AgentToolResult, AgentToolUpdateCallback, ToolExecutionMode };
export type { AppKeybinding, KeybindingsManager } from "../keybindings.js";
/** Options for extension UI dialogs. */
export interface ExtensionUIDialogOptions {
    /** AbortSignal to programmatically dismiss the dialog. */
    signal?: AbortSignal;
    /** Timeout in milliseconds. Dialog auto-dismisses with live countdown display. */
    timeout?: number;
}
/** Placement for extension widgets. */
export type WidgetPlacement = "aboveEditor" | "belowEditor";
/** Options for extension widgets. */
export interface ExtensionWidgetOptions {
    /** Where the widget is rendered. Defaults to "aboveEditor". */
    placement?: WidgetPlacement;
}
/** Raw terminal input listener for extensions. */
export type TerminalInputHandler = (data: string) => {
    consume?: boolean;
    data?: string;
} | undefined;
/** Working indicator configuration for the interactive streaming loader. */
export interface WorkingIndicatorOptions {
    /** Animation frames. Use an empty array to hide the indicator entirely. Custom frames are rendered verbatim. */
    frames?: string[];
    /** Frame interval in milliseconds for animated indicators. */
    intervalMs?: number;
}
/** Wrap the current autocomplete provider with additional behavior. */
export type AutocompleteProviderFactory = (current: AutocompleteProvider) => AutocompleteProvider;
export type EditorFactory = (tui: TUI, theme: EditorTheme, keybindings: KeybindingsManager) => EditorComponent;
/**
 * UI context for extensions to request interactive UI.
 * Each mode (interactive, RPC, print) provides its own implementation.
 */
export interface ExtensionUIContext {
    /** Show a selector and return the user's choice. */
    select(title: string, options: string[], opts?: ExtensionUIDialogOptions): Promise<string | undefined>;
    /** Show a confirmation dialog. */
    confirm(title: string, message: string, opts?: ExtensionUIDialogOptions): Promise<boolean>;
    /** Show a text input dialog. */
    input(title: string, placeholder?: string, opts?: ExtensionUIDialogOptions): Promise<string | undefined>;
    /** Show a notification to the user. */
    notify(message: string, type?: "info" | "warning" | "error"): void;
    /** Listen to raw terminal input (interactive mode only). Returns an unsubscribe function. */
    onTerminalInput(handler: TerminalInputHandler): () => void;
    /** Set status text in the footer/status bar. Pass undefined to clear. */
    setStatus(key: string, text: string | undefined): void;
    /** Set the working/loading message shown during streaming. Call with no argument to restore default. */
    setWorkingMessage(message?: string): void;
    /** Show or hide the built-in interactive working loader row during streaming. */
    setWorkingVisible(visible: boolean): void;
    /**
     * Configure the interactive working indicator shown during streaming.
     *
     * - Omit the argument to restore the default animated spinner.
     * - Use `frames: ["●"]` for a static indicator.
     * - Use `frames: []` to hide the indicator entirely.
     * - Custom frames are rendered as provided, so extensions must add their own colors.
     */
    setWorkingIndicator(options?: WorkingIndicatorOptions): void;
    /** Set the label shown for hidden thinking blocks. Call with no argument to restore default. */
    setHiddenThinkingLabel(label?: string): void;
    /** Set a widget to display above or below the editor. Accepts string array or component factory. */
    setWidget(key: string, content: string[] | undefined, options?: ExtensionWidgetOptions): void;
    setWidget(key: string, content: ((tui: TUI, theme: Theme) => Component & {
        dispose?(): void;
    }) | undefined, options?: ExtensionWidgetOptions): void;
    /** Set a custom footer component, or undefined to restore the built-in footer.
     *
     * The factory receives a FooterDataProvider for data not otherwise accessible:
     * git branch and extension statuses from setStatus(). Token stats, model info,
     * etc. are available via ctx.sessionManager and ctx.model.
     */
    setFooter(factory: ((tui: TUI, theme: Theme, footerData: ReadonlyFooterDataProvider) => Component & {
        dispose?(): void;
    }) | undefined): void;
    /** Set a custom header component (shown at startup, above chat), or undefined to restore the built-in header. */
    setHeader(factory: ((tui: TUI, theme: Theme) => Component & {
        dispose?(): void;
    }) | undefined): void;
    /** Set the terminal window/tab title. */
    setTitle(title: string): void;
    /** Show a custom component with keyboard focus. */
    custom<T>(factory: (tui: TUI, theme: Theme, keybindings: KeybindingsManager, done: (result: T) => void) => (Component & {
        dispose?(): void;
    }) | Promise<Component & {
        dispose?(): void;
    }>, options?: {
        overlay?: boolean;
        /** Overlay positioning/sizing options. Can be static or a function for dynamic updates. */
        overlayOptions?: OverlayOptions | (() => OverlayOptions);
        /** Called with the overlay handle after the overlay is shown. Use to control visibility. */
        onHandle?: (handle: OverlayHandle) => void;
    }): Promise<T>;
    /** Paste text into the editor, triggering paste handling (collapse for large content). */
    pasteToEditor(text: string): void;
    /** Set the text in the core input editor. */
    setEditorText(text: string): void;
    /** Get the current text from the core input editor. */
    getEditorText(): string;
    /** Show a multi-line editor for text editing. */
    editor(title: string, prefill?: string): Promise<string | undefined>;
    /** Stack additional autocomplete behavior on top of the built-in provider. */
    addAutocompleteProvider(factory: AutocompleteProviderFactory): void;
    /**
     * Set a custom editor component via factory function.
     * Pass undefined to restore the default editor.
     *
     * The factory receives:
     * - `theme`: EditorTheme for styling borders and autocomplete
     * - `keybindings`: KeybindingsManager for app-level keybindings
     *
     * For full app keybinding support (escape, ctrl+d, model switching, etc.),
     * extend `CustomEditor` from `@mariozechner/pi-coding-agent` and call
     * `super.handleInput(data)` for keys you don't handle.
     *
     * @example
     * ```ts
     * import { CustomEditor } from "@mariozechner/pi-coding-agent";
     *
     * class VimEditor extends CustomEditor {
     *   private mode: "normal" | "insert" = "insert";
     *
     *   handleInput(data: string): void {
     *     if (this.mode === "normal") {
     *       // Handle vim normal mode keys...
     *       if (data === "i") { this.mode = "insert"; return; }
     *     }
     *     super.handleInput(data);  // App keybindings + text editing
     *   }
     * }
     *
     * ctx.ui.setEditorComponent((tui, theme, keybindings) =>
     *   new VimEditor(tui, theme, keybindings)
     * );
     * ```
     */
    setEditorComponent(factory: EditorFactory | undefined): void;
    /** Get the currently configured custom editor factory, or undefined when using the default editor. */
    getEditorComponent(): EditorFactory | undefined;
    /** Get the current theme for styling. */
    readonly theme: Theme;
    /** Get all available themes with their names and file paths. */
    getAllThemes(): {
        name: string;
        path: string | undefined;
    }[];
    /** Load a theme by name without switching to it. Returns undefined if not found. */
    getTheme(name: string): Theme | undefined;
    /** Set the current theme by name or Theme object. */
    setTheme(theme: string | Theme): {
        success: boolean;
        error?: string;
    };
    /** Get current tool output expansion state. */
    getToolsExpanded(): boolean;
    /** Set tool output expansion state. */
    setToolsExpanded(expanded: boolean): void;
}
export interface ContextUsage {
    /** Estimated context tokens, or null if unknown (e.g. right after compaction, before next LLM response). */
    tokens: number | null;
    contextWindow: number;
    /** Context usage as percentage of context window, or null if tokens is unknown. */
    percent: number | null;
}
export interface CompactOptions {
    customInstructions?: string;
    onComplete?: (result: CompactionResult) => void;
    onError?: (error: Error) => void;
}
/**
 * Context passed to extension event handlers.
 */
export interface ExtensionContext {
    /** UI methods for user interaction */
    ui: ExtensionUIContext;
    /** Whether UI is available (false in print/RPC mode) */
    hasUI: boolean;
    /** Current working directory */
    cwd: string;
    /** Session manager (read-only) */
    sessionManager: ReadonlySessionManager;
    /** Model registry for API key resolution */
    modelRegistry: ModelRegistry;
    /** Current model (may be undefined) */
    model: Model<any> | undefined;
    /** Whether the agent is idle (not streaming) */
    isIdle(): boolean;
    /** The current abort signal, or undefined when the agent is not streaming. */
    signal: AbortSignal | undefined;
    /** Abort the current agent operation */
    abort(): void;
    /** Whether there are queued messages waiting */
    hasPendingMessages(): boolean;
    /** Gracefully shutdown pi and exit. Available in all contexts. */
    shutdown(): void;
    /** Get current context usage for the active model. */
    getContextUsage(): ContextUsage | undefined;
    /** Trigger compaction without awaiting completion. */
    compact(options?: CompactOptions): void;
    /** Get the current effective system prompt. */
    getSystemPrompt(): string;
}
/**
 * Extended context for command handlers.
 * Includes session control methods only safe in user-initiated commands.
 */
export interface ExtensionCommandContext extends ExtensionContext {
    /** Wait for the agent to finish streaming */
    waitForIdle(): Promise<void>;
    /** Start a new session, optionally with initialization. */
    newSession(options?: {
        parentSession?: string;
        setup?: (sessionManager: SessionManager) => Promise<void>;
        withSession?: (ctx: ReplacedSessionContext) => Promise<void>;
    }): Promise<{
        cancelled: boolean;
    }>;
    /** Fork from a specific entry, creating a new session file. */
    fork(entryId: string, options?: {
        position?: "before" | "at";
        withSession?: (ctx: ReplacedSessionContext) => Promise<void>;
    }): Promise<{
        cancelled: boolean;
    }>;
    /** Navigate to a different point in the session tree. */
    navigateTree(targetId: string, options?: {

[913 more lines in file. Use offset=261 to continue.]


/**
 * Extension system for lifecycle events and custom tools.
 */
export type { SlashCommandInfo, SlashCommandSource } from "../slash-commands.js";
export type { SourceInfo } from "../source-info.js";
export { createExtensionRuntime, discoverAndLoadExtensions, loadExtensionFromFactory, loadExtensions, } from "./loader.js";
export type { ExtensionErrorListener, ForkHandler, NavigateTreeHandler, NewSessionHandler, ShutdownHandler, SwitchSessionHandler, } from "./runner.js";
export { ExtensionRunner } from "./runner.js";
export type { AfterProviderResponseEvent, AgentEndEvent, AgentStartEvent, AgentToolResult, AgentToolUpdateCallback, AppendEntryHandler, AppKeybinding, AutocompleteProviderFactory, BashToolCallEvent, BashToolResultEvent, BeforeAgentStartEvent, BeforeAgentStartEventResult, BeforeProviderRequestEvent, BeforeProviderRequestEventResult, BuildSystemPromptOptions, CompactOptions, ContextEvent, ContextEventResult, ContextUsage, CustomToolCallEvent, CustomToolResultEvent, EditorFactory, EditToolCallEvent, EditToolResultEvent, ExecOptions, ExecResult, Extension, ExtensionActions, ExtensionAPI, ExtensionCommandContext, ExtensionCommandContextActions, ExtensionContext, ExtensionContextActions, ExtensionError, ExtensionEvent, ExtensionFactory, ExtensionFlag, ExtensionHandler, ExtensionRuntime, ExtensionShortcut, ExtensionUIContext, ExtensionUIDialogOptions, ExtensionWidgetOptions, FindToolCallEvent, FindToolResultEvent, GetActiveToolsHandler, GetAllToolsHandler, GetCommandsHandler, GetThinkingLevelHandler, GrepToolCallEvent, GrepToolResultEvent, InputEvent, InputEventResult, InputSource, KeybindingsManager, LoadExtensionsResult, LsToolCallEvent, LsToolResultEvent, MessageEndEvent, MessageRenderer, MessageRenderOptions, MessageStartEvent, MessageUpdateEvent, ModelSelectEvent, ModelSelectSource, ProviderConfig, ProviderModelConfig, ReadToolCallEvent, ReadToolResultEvent, RegisteredCommand, RegisteredTool, ReplacedSessionContext, ResolvedCommand, ResourcesDiscoverEvent, ResourcesDiscoverResult, SendMessageHandler, SendUserMessageHandler, SessionBeforeCompactEvent, SessionBeforeCompactResult, SessionBeforeForkEvent, SessionBeforeForkResult, SessionBeforeSwitchEvent, SessionBeforeSwitchResult, SessionBeforeTreeEvent, SessionBeforeTreeResult, SessionCompactEvent, SessionEvent, SessionShutdownEvent, SessionStartEvent, SessionTreeEvent, SetActiveToolsHandler, SetLabelHandler, SetModelHandler, SetThinkingLevelHandler, TerminalInputHandler, ToolCallEvent, ToolCallEventResult, ToolDefinition, ToolExecutionEndEvent, ToolExecutionMode, ToolExecutionStartEvent, ToolExecutionUpdateEvent, ToolInfo, ToolRenderResultOptions, ToolResultEvent, ToolResultEventResult, TreePreparation, TurnEndEvent, TurnStartEvent, UserBashEvent, UserBashEventResult, WidgetPlacement, WorkingIndicatorOptions, WriteToolCallEvent, WriteToolResultEvent, } from "./types.js";
export { defineTool, isBashToolResult, isEditToolResult, isFindToolResult, isGrepToolResult, isLsToolResult, isReadToolResult, isToolCallEventType, isWriteToolResult, } from "./types.js";
export { wrapRegisteredTool, wrapRegisteredTools } from "./wrapper.js";
//# sourceMappingURL=index.d.ts.map


bash

cd /home/sebas/pi-config && rg -n "sendUserMessage|sendMessage|prompt\(|followUp\(|steer\(|appendUser|slash command|ResolvedCommand|registerCommand\(" node_modules/@mariozechner/pi-coding-agent/dist/core/extensions/types.d.ts


283:    sendMessage<T = unknown>(message: Pick<CustomMessage<T>, "customType" | "content" | "display" | "details">, options?: {
287:    sendUserMessage(content: string | (TextContent | ImageContent)[], options?: {
775:export interface ResolvedCommand extends RegisteredCommand {
816:    registerCommand(name: string, options: Omit<RegisteredCommand, "name" | "sourceInfo">): void;
833:    sendMessage<T = unknown>(message: Pick<CustomMessage<T>, "customType" | "content" | "display" | "details">, options?: {
841:    sendUserMessage(content: string | (TextContent | ImageContent)[], options?: {
860:    /** Get available slash commands in the current session. */
1075:    sendMessage: SendMessageHandler;
1076:    sendUserMessage: SendUserMessageHandler;


        withSession?: (ctx: ReplacedSessionContext) => Promise<void>;
    }): Promise<{
        cancelled: boolean;
    }>;
    /** Reload extensions, skills, prompts, and themes. */
    reload(): Promise<void>;
}
/**
 * Fresh command-capable context bound to the replacement session after a session switch.
 *
 * This is passed to `withSession()` callbacks on `newSession()`, `fork()`, and `switchSession()`.
 */
export interface ReplacedSessionContext extends ExtensionCommandContext {
    sendMessage<T = unknown>(message: Pick<CustomMessage<T>, "customType" | "content" | "display" | "details">, options?: {
        triggerTurn?: boolean;
        deliverAs?: "steer" | "followUp" | "nextTurn";
    }): Promise<void>;
    sendUserMessage(content: string | (TextContent | ImageContent)[], options?: {
        deliverAs?: "steer" | "followUp";
    }): Promise<void>;
}
/** Rendering options for tool results */
export interface ToolRenderResultOptions {
    /** Whether the result view is expanded */
    expanded: boolean;
    /** Whether this is a partial/streaming result */
    isPartial: boolean;
}
/** Context passed to tool renderers. */
export interface ToolRenderContext<TState = any, TArgs = any> {
    /** Current tool call arguments. Shared across call/result renders for the same tool call. */
    args: TArgs;
    /** Unique id for this tool execution. Stable across call/result renders for the same tool call. */
    toolCallId: string;
    /** Invalidate just this tool execution component for redraw. */
    invalidate: () => void;
    /** Previously returned component for this render slot, if any. */
    lastComponent: Component | undefined;
    /** Shared renderer state for this tool row. Initialized by tool-execution.ts. */
    state: TState;
    /** Working directory for this tool execution. */
    cwd: string;
    /** Whether the tool execution has started. */
    executionStarted: boolean;
    /** Whether the tool call arguments are complete. */
    argsComplete: boolean;
    /** Whether the tool result is partial/streaming. */
    isPartial: boolean;
    /** Whether the result view is expanded. */
    expanded: boolean;
    /** Whether inline images are currently shown in the TUI. */
    showImages: boolean;
    /** Whether the current result is an error. */
    isError: boolean;
}
/**
 * Tool definition for registerTool().
 */
export interface ToolDefinition<TParams extends TSchema = TSchema, TDetails = unknown, TState = any> {
    /** Tool name (used in LLM tool calls) */
    name: string;
    /** Human-readable label for UI */
    label: string;
    /** Description for LLM */
    description: string;
    /** Optional one-line snippet for the Available tools section in the default system prompt. Custom tools are omitted from that section when this is not provided. */
    promptSnippet?: string;
    /** Optional guideline bullets appended to the default system prompt Guidelines section when this tool is active. */
    promptGuidelines?: string[];
    /** Parameter schema (TypeBox) */
    parameters: TParams;
    /** Controls whether ToolExecutionComponent renders the standard colored shell or the tool renders its own framing. */
    renderShell?: "default" | "self";
    /** Optional compatibility shim to prepare raw tool call arguments before schema validation. Must return an object conforming to TParams. */
    prepareArguments?: (args: unknown) => Static<TParams>;
    /**
     * Per-tool execution mode override.
     * - "sequential": this tool must execute one at a time with other tool calls.
     * - "parallel": this tool can execute concurrently with other tool calls.
     *
     * If omitted, the default execution mode applies.
     */
    executionMode?: ToolExecutionMode;
    /** Execute the tool. */
    execute(toolCallId: string, params: Static<TParams>, signal: AbortSignal | undefined, onUpdate: AgentToolUpdateCallback<TDetails> | undefined, ctx: ExtensionContext): Promise<AgentToolResult<TDetails>>;
    /** Custom rendering for tool call display */
    renderCall?: (args: Static<TParams>, theme: Theme, context: ToolRenderContext<TState, Static<TParams>>) => Component;
    /** Custom rendering for tool result display */
    renderResult?: (result: AgentToolResult<TDetails>, options: ToolRenderResultOptions, theme: Theme, context: ToolRenderContext<TState, Static<TParams>>) => Component;
}

[814 more lines in file. Use offset=360 to continue.]


    on(event: "turn_end", handler: ExtensionHandler<TurnEndEvent>): void;
    on(event: "message_start", handler: ExtensionHandler<MessageStartEvent>): void;
    on(event: "message_update", handler: ExtensionHandler<MessageUpdateEvent>): void;
    on(event: "message_end", handler: ExtensionHandler<MessageEndEvent, MessageEndEventResult>): void;
    on(event: "tool_execution_start", handler: ExtensionHandler<ToolExecutionStartEvent>): void;
    on(event: "tool_execution_update", handler: ExtensionHandler<ToolExecutionUpdateEvent>): void;
    on(event: "tool_execution_end", handler: ExtensionHandler<ToolExecutionEndEvent>): void;
    on(event: "model_select", handler: ExtensionHandler<ModelSelectEvent>): void;
    on(event: "thinking_level_select", handler: ExtensionHandler<ThinkingLevelSelectEvent>): void;
    on(event: "tool_call", handler: ExtensionHandler<ToolCallEvent, ToolCallEventResult>): void;
    on(event: "tool_result", handler: ExtensionHandler<ToolResultEvent, ToolResultEventResult>): void;
    on(event: "user_bash", handler: ExtensionHandler<UserBashEvent, UserBashEventResult>): void;
    on(event: "input", handler: ExtensionHandler<InputEvent, InputEventResult>): void;
    /** Register a tool that the LLM can call. */
    registerTool<TParams extends TSchema = TSchema, TDetails = unknown, TState = any>(tool: ToolDefinition<TParams, TDetails, TState>): void;
    /** Register a custom command. */
    registerCommand(name: string, options: Omit<RegisteredCommand, "name" | "sourceInfo">): void;
    /** Register a keyboard shortcut. */
    registerShortcut(shortcut: KeyId, options: {
        description?: string;
        handler: (ctx: ExtensionContext) => Promise<void> | void;
    }): void;
    /** Register a CLI flag. */
    registerFlag(name: string, options: {
        description?: string;
        type: "boolean" | "string";
        default?: boolean | string;
    }): void;
    /** Get the value of a registered CLI flag. */
    getFlag(name: string): boolean | string | undefined;
    /** Register a custom renderer for CustomMessageEntry. */
    registerMessageRenderer<T = unknown>(customType: string, renderer: MessageRenderer<T>): void;
    /** Send a custom message to the session. */
    sendMessage<T = unknown>(message: Pick<CustomMessage<T>, "customType" | "content" | "display" | "details">, options?: {
        triggerTurn?: boolean;
        deliverAs?: "steer" | "followUp" | "nextTurn";
    }): void;
    /**
     * Send a user message to the agent. Always triggers a turn.
     * When the agent is streaming, use deliverAs to specify how to queue the message.
     */
    sendUserMessage(content: string | (TextContent | ImageContent)[], options?: {
        deliverAs?: "steer" | "followUp";
    }): void;
    /** Append a custom entry to the session for state persistence (not sent to LLM). */
    appendEntry<T = unknown>(customType: string, data?: T): void;
    /** Set the session display name (shown in session selector). */
    setSessionName(name: string): void;
    /** Get the current session name, if set. */
    getSessionName(): string | undefined;
    /** Set or clear a label on an entry. Labels are user-defined markers for bookmarking/navigation. */
    setLabel(entryId: string, label: string | undefined): void;
    /** Execute a shell command. */
    exec(command: string, args: string[], options?: ExecOptions): Promise<ExecResult>;
    /** Get the list of currently active tool names. */
    getActiveTools(): string[];
    /** Get all configured tools with parameter schema and source metadata. */
    getAllTools(): ToolInfo[];
    /** Set the active tools by name. */
    setActiveTools(toolNames: string[]): void;
    /** Get available slash commands in the current session. */
    getCommands(): SlashCommandInfo[];
    /** Set the current model. Returns false if no API key available. */
    setModel(model: Model<any>): Promise<boolean>;
    /** Get current thinking level. */
    getThinkingLevel(): ThinkingLevel;
    /** Set thinking level (clamped to model capabilities). */
    setThinkingLevel(level: ThinkingLevel): void;
    /**
     * Register or override a model provider.
     *
     * If `models` is provided: replaces all existing models for this provider.
     * If only `baseUrl` is provided: overrides the URL for existing models.
     * If `oauth` is provided: registers OAuth provider for /login support.
     * If `streamSimple` is provided: registers a custom API stream handler.
     *
     * During initial extension load this call is queued and applied once the
     * runner has bound its context. After that it takes effect immediately, so
     * it is safe to call from command handlers or event callbacks without
     * requiring a `/reload`.
     *
     * @example
     * // Register a new provider with custom models
     * pi.registerProvider("my-proxy", {
     *   baseUrl: "https://proxy.example.com",
     *   apiKey: "PROXY_API_KEY",
     *   api: "anthropic-messages",
     *   models: [
     *     {
     *       id: "claude-sonnet-4-20250514",

[284 more lines in file. Use offset=890 to continue.]


cd /home/sebas/pi-config && rg -n "interface RegisteredCommand|type RegisteredCommand|handler: \(args" node_modules/@mariozechner/pi-coding-agent/dist/core/extensions/types.d.ts


768:export interface RegisteredCommand {
773:    handler: (args: string, ctx: ExtensionCommandContext) => Promise<void>;


    replaceInstructions?: boolean;
    /** Override label to attach to the branch summary entry */
    label?: string;
}
export interface MessageRenderOptions {
    expanded: boolean;
}
export type MessageRenderer<T = unknown> = (message: CustomMessage<T>, options: MessageRenderOptions, theme: Theme) => Component | undefined;
export interface RegisteredCommand {
    name: string;
    sourceInfo: SourceInfo;
    description?: string;
    getArgumentCompletions?: (argumentPrefix: string) => AutocompleteItem[] | null | Promise<AutocompleteItem[] | null>;
    handler: (args: string, ctx: ExtensionCommandContext) => Promise<void>;
}
export interface ResolvedCommand extends RegisteredCommand {
    invocationName: string;
}
/** Handler function type for events */
export type ExtensionHandler<E, R = undefined> = (event: E, ctx: ExtensionContext) => Promise<R | void> | R | void;
/**
 * ExtensionAPI passed to extension factory functions.
 */
export interface ExtensionAPI {
    on(event: "resources_discover", handler: ExtensionHandler<ResourcesDiscoverEvent, ResourcesDiscoverResult>): void;

[389 more lines in file. Use offset=785 to continue.]


cd /home/sebas/pi-config && rg -n "interface ResourcesDiscoverResult|type ResourcesDiscoverResult|promptPaths|skillPaths" node_modules/@mariozechner/pi-coding-agent/dist/core/extensions/types.d.ts


376:export interface ResourcesDiscoverResult {
377:    skillPaths?: string[];
378:    promptPaths?: string[];


export declare function defineTool<TParams extends TSchema, TDetails = unknown, TState = any>(tool: ToolDefinition<TParams, TDetails, TState>): ToolDefinition<TParams, TDetails, TState> & AnyToolDefinition;
/** Fired after session_start to allow extensions to provide additional resource paths. */
export interface ResourcesDiscoverEvent {
    type: "resources_discover";
    cwd: string;
    reason: "startup" | "reload";
}
/** Result from resources_discover event handler */
export interface ResourcesDiscoverResult {
    skillPaths?: string[];
    promptPaths?: string[];
    themePaths?: string[];
}
/** Fired when a session is started, loaded, or reloaded */
export interface SessionStartEvent {
    type: "session_start";
    /** Why this session start happened. */
    reason: "startup" | "reload" | "new" | "resume" | "fork";

[788 more lines in file. Use offset=386 to continue.]


**Testing command execution**

I’m considering testing the command invocation using the "/start" command, but I'm worried it might hang if I send it without arguments. Instead, I could use the command handler with "/status --quick" or "/summary.” However, I'm thinking that might not be the best choice for performance. Since the user wants to validate the load, maybe just listing the commands would suffice for now. Let's keep it simple!


import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
import { Type } from "@sinclair/typebox";
import {
	HUMAND_PROMPT,
	buildBranchName,
	checkoutInPlaceBranch,
	createWorktreeForBranch,
	detectHumandMode,
	extractTicketKey,
	getBootstrapEnv,
	getHumandStatus,
	getOriginUrl,
	loadHumandConfig,
	resolveFeatureFromCwd,
	resolveLocalRepoPaths,
	sanitizeBranchDescription,
} from "./_shared/humand-shared.ts";

const NOTICE = [
	"Humand mode active.",
	"Isolated Humand workflow available.",
	"Source: pi-config/humand + hu-ai-agent-plugin clone.",
	"Use explicit Humand skills/commands only; non-Humand work stays unchanged.",
].join(" ");

function describeHumandContext(cwd: string): string[] {
	const env = getBootstrapEnv(cwd);
	return [
		`cwd=${cwd}`,
		`humand_tree_or_remote=${detectHumandMode(cwd) ? "yes" : "no"}`,
		`origin=${getOriginUrl(cwd) || "-"}`,
		`humand_prompt=${HUMAND_PROMPT}`,
		`HUMAND_PI_CONFIG_HOME=${env.HUMAND_PI_CONFIG_HOME || ""}`,
		`HUMAND_PI_STATE_HOME=${env.HUMAND_PI_STATE_HOME || ""}`,
		`HUMAND_PI_LOCAL_CONFIG_FILE=${env.HUMAND_PI_LOCAL_CONFIG_FILE || ""}`,
	];
}

export default function humandMode(pi: ExtensionAPI) {
	let lastAnnouncedKey: string | null = null;

	const maybeAnnounce = (ctx: any, reason?: string) => {
		if (!ctx?.hasUI) return;
		if (!detectHumandMode(ctx.cwd)) return;
		const key = `${ctx.cwd}:${reason || ""}`;
		if (lastAnnouncedKey === key) return;
		lastAnnouncedKey = key;
		ctx.ui.notify(`${NOTICE} ${describeHumandContext(ctx.cwd).join(" | ")}`, "info");
	};

	pi.on("resources_discover", async (event) => {
		if (!detectHumandMode(event.cwd)) return undefined;
		return { promptPaths: [HUMAND_PROMPT] };
	});

	pi.on("session_start", async (event, ctx) => {
		if (event.reason === "reload") return;
		maybeAnnounce(ctx, event.reason);
	});

	pi.registerCommand("humand-mode", {
		description: "Show whether isolated Humand mode is active for this session",
		handler: async (_args, ctx) => {
			const active = detectHumandMode(ctx.cwd);
			const text = active
				? `${NOTICE} ${describeHumandContext(ctx.cwd).join(" | ")}`
				: `Humand mode inactive for current cwd. cwd=${ctx.cwd}`;
			ctx.ui.notify(text, active ? "info" : "warning");
		},
	});

	pi.registerCommand("humand-status", {
		description: "Show isolated Humand workflow status and bootstrap coverage",
		handler: async (_args, ctx) => {
			const status = getHumandStatus(ctx.cwd);
			const lines = [
				`active=${status.active}`,
				`cwd=${status.cwd}`,
				`origin=${status.origin || "-"}`,
				`local_config=${status.localConfigFile || "MISSING"}`,
				`bundled_repo_count=${status.bundledRepoCount}`,
				`configured_repo_count=${status.configuredRepoCount}`,
				`existing_repo_count=${status.existingRepoCount}`,
				`team_count=${status.teamCount}`,
				`missing_repo_keys=${status.missingRepoKeys.join(",") || "-"}`,
			];
			ctx.ui.notify(lines.join(" | "), status.active ? "info" : "warning");
		},
	});

	pi.registerTool({
		name: "humand_context",
		label: "Humand Context",
		description: "Return isolated Humand workflow activation status and resolved bootstrap variables for the current cwd",
		parameters: Type.Object({}),
		async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
			const active = detectHumandMode(ctx.cwd);
			const env = getBootstrapEnv(ctx.cwd);
			const details = {
				active,
				cwd: ctx.cwd,
				origin: getOriginUrl(ctx.cwd),
				prompt: HUMAND_PROMPT,
				env,
			};
			const lines = [
				`active=${active}`,
				`cwd=${ctx.cwd}`,
				`origin=${details.origin || "-"}`,
				`prompt=${HUMAND_PROMPT}`,
				...Object.entries(env).map(([k, v]) => `${k}=${v}`),
			];
			return {
				content: [{ type: "text", text: lines.join("\n") }],
				details,
			};
		},
	});

	pi.registerTool({
		name: "humand_status",
		label: "Humand Status",
		description: "Return isolated Humand workflow status including repo config coverage for the current cwd",
		parameters: Type.Object({}),
		async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
			const status = getHumandStatus(ctx.cwd);
			const lines = [
				`active=${status.active}`,
				`cwd=${status.cwd}`,
				`origin=${status.origin || "-"}`,
				`prompt=${status.prompt}`,
				`repos_file=${status.reposFile || ""}`,
				`teams_file=${status.teamsFile || ""}`,
				`local_config=${status.localConfigFile || ""}`,
				`has_local_config=${status.hasLocalConfig}`,
				`bundled_repo_count=${status.bundledRepoCount}`,
				`configured_repo_count=${status.configuredRepoCount}`,
				`existing_repo_count=${status.existingRepoCount}`,
				`team_count=${status.teamCount}`,
				`missing_repo_keys=${status.missingRepoKeys.join(",") || "-"}`,
			];
			return {
				content: [{ type: "text", text: lines.join("\n") }],
				details: status,
			};
		},
	});

	pi.registerTool({
		name: "humand_feature_resolution",
		label: "Humand Feature Resolution",
		description: "Resolve the active Humand feature branch and repo paths from current cwd or a branch override",
		parameters: Type.Object({
			branch: Type.Optional(Type.String({ description: "Optional branch name override" })),
		}),
		async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
			const resolved = resolveFeatureFromCwd(ctx.cwd, params.branch);
			const lines = [
				`branch=${resolved.branch || ""}`,
				`ticket=${resolved.ticket || ""}`,
				`repo_count=${resolved.repoOrder.length}`,
				...resolved.repoOrder.map((repoKey) => `${repoKey}=${resolved.repos[repoKey]}`),
			];
			return {
				content: [{ type: "text", text: lines.join("\n") }],
				details: resolved,
			};
		},
	});

	pi.registerTool({
		name: "humand_start_feature",
		label: "Humand Start Feature",
		description: "Create isolated Humand feature branches/worktrees for selected repos using the local workflow config",
		parameters: Type.Object({
			ticket: Type.Optional(Type.String({ description: "Ticket key like SQSH-1234. Optional." })),
			branchType: Type.String({ description: "Branch type, e.g. fix, feat, chore" }),
			description: Type.String({ description: "Short branch description or ticket title" }),
			repos: Type.Array(Type.String({ description: "Repo keys from the Humand workflow config" })),
			mode: Type.Optional(Type.Union([Type.Literal("worktree"), Type.Literal("in-place")], { default: "worktree" })),
		}),
		async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
			const { localConfigFile, reposJson, localConfigJson } = loadHumandConfig(ctx.cwd);
			const localRepoPaths = resolveLocalRepoPaths(localConfigFile, localConfigJson);
			const repoMeta = reposJson?.repos || {};
			const branchName = buildBranchName(params.branchType, params.ticket, params.description);
			const mode = params.mode || "worktree";
			const results: Array<{ repo: string; path: string; baseBranch: string }> = [];

			for (const repoKey of params.repos) {
				const repoPath = localRepoPaths[repoKey];
				if (!repoPath) throw new Error(`Repo not configured locally: ${repoKey}`);
				const baseBranch = repoMeta[repoKey]?.base_branch || "develop";
				const targetPath = mode === "worktree"
					? createWorktreeForBranch(repoPath, branchName, baseBranch)
					: checkoutInPlaceBranch(repoPath, branchName, baseBranch);
				results.push({ repo: repoKey, path: targetPath, baseBranch });
			}

			const lines = [
				`branch=${branchName}`,
				`ticket=${extractTicketKey(branchName) || params.ticket || ""}`,
				`mode=${mode}`,
				...results.map((item) => `${item.repo}=${item.path} (base=${item.baseBranch})`),
			];
			return {
				content: [{ type: "text", text: lines.join("\n") }],
				details: { branchName, mode, repos: results, description: sanitizeBranchDescription(params.description) },
			};
		},
	});
}


**Modifying constants and commands**

I’m thinking about how to edit by adding constants and a command wrapper. It seems I might need to import the skill root constant, either from shared resources or local constants. So, I'll go ahead and make those modifications. This step should help streamline the process and ensure everything runs smoothly! I’m curious if there’s anything else I might need to consider while editing. Let's keep going!

edit

const NOTICE = [
	"Humand mode active.",
	"Isolated Humand workflow available.",
	"Source: pi-config/humand + hu-ai-agent-plugin clone.",
	"Use explicit Humand skills/commands only; non-Humand work stays unchanged.",
].join(" ");

const HUMAND_SKILLS_ROOT = "/home/sebas/work/humand/hu-ai-agent-plugin/plugins/humand-tech/skills";
const HUMAND_COMMAND_SKILLS = ["start", "status", "pr", "merge-watch", "summary"] as const;

type HumandCommandSkill = (typeof HUMAND_COMMAND_SKILLS)[number];

export default function humandMode(pi: ExtensionAPI) {
	let lastAnnouncedKey: string | null = null;

	const maybeAnnounce = (ctx: any, reason?: string) => {

	pi.on("resources_discover", async (event) => {
		if (!detectHumandMode(event.cwd)) return undefined;
		return { promptPaths: [HUMAND_PROMPT] };
	});

	const runHumandSkillCommand = async (skillName: HumandCommandSkill, args: string, ctx: any) => {
		if (!detectHumandMode(ctx.cwd)) {
			ctx.ui.notify(`Humand command /${skillName} only works in Humand repos/worktrees. cwd=${ctx.cwd}`, "warning");
			return;
		}
		const trimmed = args.trim();
		const request = [
			`Handle this as the Humand slash command /${skillName}.`,
			`Load and follow the active Humand skill named \"${skillName}\" from the session resources.`,
			trimmed ? `Original command arguments: ${trimmed}` : "Original command arguments: (none)",
			"Execute the command workflow now.",
		].join("\n");
		pi.sendUserMessage(request);
	};

	pi.on("resources_discover", async (event) => {
		if (!detectHumandMode(event.cwd)) return undefined;
		return {
			promptPaths: [HUMAND_PROMPT],
			skillPaths: [HUMAND_SKILLS_ROOT],
		};
	});

	pi.registerCommand("humand-status", {
		description: "Show isolated Humand workflow status and bootstrap coverage",
		handler: async (_args, ctx) => {
			const status = getHumandStatus(ctx.cwd);
			const lines = [
				`active=${status.active}`,
				`cwd=${status.cwd}`,
				`origin=${status.origin || "-"}`,
				`local_config=${status.localConfigFile || "MISSING"}`,
				`bundled_repo_count=${status.bundledRepoCount}`,
				`configured_repo_count=${status.configuredRepoCount}`,
				`existing_repo_count=${status.existingRepoCount}`,
				`team_count=${status.teamCount}`,
				`missing_repo_keys=${status.missingRepoKeys.join(",") || "-"}`,
			];
			ctx.ui.notify(lines.join(" | "), status.active ? "info" : "warning");
		},
	});

	for (const skillName of HUMAND_COMMAND_SKILLS) {
		pi.registerCommand(skillName, {
			description: `Run Humand /${skillName} workflow in Humand context`,
			handler: async (args, ctx) => runHumandSkillCommand(skillName, args, ctx),
		});
	}


Successfully replaced 4 block(s) in /home/sebas/pi-config/extensions/humand-mode.ts.

     ...
  22 	"Source: pi-config/humand + hu-ai-agent-plugin clone.",
  23 	"Use explicit Humand skills/commands only; non-Humand work stays unchanged.",
  24 ].join(" ");
  25
+ 26 const HUMAND_SKILLS_ROOT = "/home/sebas/work/humand/hu-ai-agent-plugin/plugins/humand-tech/skills";
+ 27 const HUMAND_COMMAND_SKILLS = ["start", "status", "pr", "merge-watch", "summary"] as const;
+ 28
+ 29 type HumandCommandSkill = (typeof HUMAND_COMMAND_SKILLS)[number];
+ 30
  26 function describeHumandContext(cwd: string): string[] {
  27 	const env = getBootstrapEnv(cwd);
  28 	return [
  29 		`cwd=${cwd}`,
     ...
  47 		lastAnnouncedKey = key;
  48 		ctx.ui.notify(`${NOTICE} ${describeHumandContext(ctx.cwd).join(" | ")}`, "info");
  49 	};
  50
+ 56 	const runHumandSkillCommand = async (skillName: HumandCommandSkill, args: string, ctx: any) => {
+ 57 		if (!detectHumandMode(ctx.cwd)) {
+ 58 			ctx.ui.notify(`Humand command /${skillName} only works in Humand repos/worktrees. cwd=${ctx.cwd}`, "warning");
+ 59 			return;
+ 60 		}
+ 61 		const trimmed = args.trim();
+ 62 		const request = [
+ 63 			`Handle this as the Humand slash command /${skillName}.`,
+ 64 			`Load and follow the active Humand skill named \"${skillName}\" from the session resources.`,
+ 65 			trimmed ? `Original command arguments: ${trimmed}` : "Original command arguments: (none)",
+ 66 			"Execute the command workflow now.",
+ 67 		].join("\n");
+ 68 		pi.sendUserMessage(request);
+ 69 	};
+ 70
  51 	pi.on("resources_discover", async (event) => {
  52 		if (!detectHumandMode(event.cwd)) return undefined;
- 53 		return { promptPaths: [HUMAND_PROMPT] };
+ 73 		return {
+ 74 			promptPaths: [HUMAND_PROMPT],
+ 75 			skillPaths: [HUMAND_SKILLS_ROOT],
+ 76 		};
  54 	});
  55
  56 	pi.on("session_start", async (event, ctx) => {
  57 		if (event.reason === "reload") return;
     ...
  87 			ctx.ui.notify(lines.join(" | "), status.active ? "info" : "warning");
  88 		},
  89 	});
  90
+114 	for (const skillName of HUMAND_COMMAND_SKILLS) {
+115 		pi.registerCommand(skillName, {
+116 			description: `Run Humand /${skillName} workflow in Humand context`,
+117 			handler: async (args, ctx) => runHumandSkillCommand(skillName, args, ctx),
+118 		});
+119 	}
+120
  91 	pi.registerTool({
  92 		name: "humand_context",
  93 		label: "Humand Context",
  94 		description: "Return isolated Humand workflow activation status and resolved bootstrap variables for the current cwd",
     ...

--- /home/sebas/pi-config/extensions/humand-mode.ts
+++ /home/sebas/pi-config/extensions/humand-mode.ts
@@ -22,8 +22,13 @@
 	"Source: pi-config/humand + hu-ai-agent-plugin clone.",
 	"Use explicit Humand skills/commands only; non-Humand work stays unchanged.",
 ].join(" ");

+const HUMAND_SKILLS_ROOT = "/home/sebas/work/humand/hu-ai-agent-plugin/plugins/humand-tech/skills";
+const HUMAND_COMMAND_SKILLS = ["start", "status", "pr", "merge-watch", "summary"] as const;
+
+type HumandCommandSkill = (typeof HUMAND_COMMAND_SKILLS)[number];
+
 function describeHumandContext(cwd: string): string[] {
 	const env = getBootstrapEnv(cwd);
 	return [
 		`cwd=${cwd}`,
@@ -47,11 +52,29 @@
 		lastAnnouncedKey = key;
 		ctx.ui.notify(`${NOTICE} ${describeHumandContext(ctx.cwd).join(" | ")}`, "info");
 	};

+	const runHumandSkillCommand = async (skillName: HumandCommandSkill, args: string, ctx: any) => {
+		if (!detectHumandMode(ctx.cwd)) {
+			ctx.ui.notify(`Humand command /${skillName} only works in Humand repos/worktrees. cwd=${ctx.cwd}`, "warning");
+			return;
+		}
+		const trimmed = args.trim();
+		const request = [
+			`Handle this as the Humand slash command /${skillName}.`,
+			`Load and follow the active Humand skill named \"${skillName}\" from the session resources.`,
+			trimmed ? `Original command arguments: ${trimmed}` : "Original command arguments: (none)",
+			"Execute the command workflow now.",
+		].join("\n");
+		pi.sendUserMessage(request);
+	};
+
 	pi.on("resources_discover", async (event) => {
 		if (!detectHumandMode(event.cwd)) return undefined;
-		return { promptPaths: [HUMAND_PROMPT] };
+		return {
+			promptPaths: [HUMAND_PROMPT],
+			skillPaths: [HUMAND_SKILLS_ROOT],
+		};
 	});

 	pi.on("session_start", async (event, ctx) => {
 		if (event.reason === "reload") return;
@@ -87,8 +110,15 @@
 			ctx.ui.notify(lines.join(" | "), status.active ? "info" : "warning");
 		},
 	});

+	for (const skillName of HUMAND_COMMAND_SKILLS) {
+		pi.registerCommand(skillName, {
+			description: `Run Humand /${skillName} workflow in Humand context`,
+			handler: async (args, ctx) => runHumandSkillCommand(skillName, args, ctx),
+		});
+	}
+
 	pi.registerTool({
 		name: "humand_context",
 		label: "Humand Context",
 		description: "Return isolated Humand workflow activation status and resolved bootstrap variables for the current cwd",


write

# Humand porting status

## Goal

Port the Humand workflow layer into this Pi setup without changing generic non-Humand behavior.

## Runtime model

- Activation gate: Humand repo/worktree only
- Detection: cwd under `/home/sebas/work/humand` or git remote containing `HumandDev` / `sfavaron-hu`
- Bootstrap source: `/home/sebas/pi-config/humand/workflow-setup.sh`
- Pi runtime entrypoint: `/home/sebas/pi-config/extensions/humand-mode.ts`
- Shared helper: `/home/sebas/pi-config/extensions/_shared/humand-shared.ts`
- Upstream source plugin: `/home/sebas/work/humand/hu-ai-agent-plugin/plugins/humand-tech`

## Ported now

### Bootstrap / context

- Humand mode detection by cwd/origin
- Bootstrap env load from `workflow-setup.sh`
- Humand prompt injection: `HUMAND.md`
- Session notice via `session_start`

### Tools

- `humand_context`
- `humand_status`
- `humand_feature_resolution`
- `humand_start_feature`

### Commands

These slash-style commands are registered by the Pi extension and dispatch into the matching loaded Humand skill:

- `/start`
- `/status`
- `/pr`
- `/merge-watch`
- `/summary`

### Skill loading

In Humand context, the extension exposes upstream Humand skill resources from:

- `/home/sebas/work/humand/hu-ai-agent-plugin/plugins/humand-tech/skills`

This is how the command flows above resolve their matching skills.

## Intentionally partial / not wired as runtime hooks

### Upstream hook reference

- Reference file: `/home/sebas/pi-config/humand/hooks/session-start`
- Status: copied for reference only
- Not auto-executed by Pi runtime on this machine
- Real activation path here is: `humand-mode.ts` + Humand bootstrap + loaded Humand skills

### Not fully ported as first-class native Pi features

These upstream workflows still run through skill instructions, not bespoke local reimplementation:

- `/start`
- `/status`
- `/pr`
- `/merge-watch`
- `/summary`

That is acceptable for current scope as long as the commands and skills load correctly in Humand context.

## Validation checklist

- [x] `pi` starts with Humand extension enabled
- [x] Humand bootstrap vars resolve
- [x] Humand tools answer in Humand repos
- [x] Humand skill path is injected in Humand repos
- [x] Commands `/start`, `/status`, `/pr`, `/merge-watch`, `/summary` register in Pi
- [ ] Interactive session-start notice visually confirmed in TUI
- [ ] End-to-end behavior of every upstream workflow validated against external deps

## Notes

- Do not place helper modules as top-level files in `extensions/` unless they export a real Pi extension factory. Pi auto-loads top-level extension files.
- Keep Humand-specific runtime surface gated by Humand detection so Ballbox/personal sessions stay unchanged in practice.


Successfully wrote 2729 bytes to /home/sebas/pi-config/humand/PORTING-STATUS.md


**Validating commands and skills**

I need to validate the commands and skills load. It looks like I should use RPC to get commands from the human repository, and maybe check the non-human ones too. I wonder if jq will be useful for parsing the information. It’s all about making sure everything is set up correctly, so I can be ready to assist. Let’s figure out how to streamline that process!

bash

python3 - <<'PY'
import json, subprocess, os, sys
cwd='/home/sebas/work/humand/humand-web'
cmd=['pi','--mode','rpc','--no-session']
p=subprocess.Popen(cmd,cwd=cwd,stdin=subprocess.PIPE,stdout=subprocess.PIPE,stderr=subprocess.PIPE,text=True)
p.stdin.write(json.dumps({'id':'1','type':'get_commands'})+'\n')
p.stdin.flush()
commands=[]
for _ in range(20):
    line=p.stdout.readline()
    if not line:
        break
    obj=json.loads(line)
    if obj.get('type')=='response' and obj.get('command')=='get_commands' and obj.get('success'):
        commands=obj['data']['commands']
        break
wanted=['start','status','pr','merge-watch','summary','skill:start','skill:status','skill:pr','skill:merge-watch','skill:summary']
found=[c['name'] for c in commands if c['name'] in wanted]
print('\n'.join(found))
p.terminate()
try:
    p.wait(timeout=3)
except subprocess.TimeoutExpired:
    p.kill()
PY

cd /home/sebas/work/humand/humand-web && pi --mode text -p "What Humand skills named start, status, pr, merge-watch, summary are available in this session? List exact names only." --no-session --thinking off


start
status
pr
merge-watch
summary
skill:merge-watch
skill:pr
skill:start
skill:status
skill:summary


start
status
pr
merge-watch
summary


**Testing command validation**

I need to validate command dispatch using the prompt "/status --quick" and check if it responds correctly. This might be costly, so maybe I'll run it as a minimal command. The skill should work in read-only mode without external authorization. I'll try running the command non-interactively, accepting some verbosity. The user asked to validate load, not full behavior. I think testing just one command dispatch will be enough. So, I'll proceed with that!

cd /home/sebas/work/humand/humand-web && pi --mode text -p "/status --quick" --no-session --thinking off


Extension error (/home/sebas/pi-config/git/github.com/apmantza/pi-free/index.ts): This extension ctx is stale after session replacement or reload. Do not use a captured pi or command ctx after ctx.newSession(), ctx.fork(), ctx.switchSession(), or ctx.reload(). For newSession, fork, and switchSession, move post-replacement work into withSession and use the ctx passed to withSession. For reload, do not use the old ctx after await ctx.reload().


**Fixing command handler issue**

I’m trying to sort out a bug related to using captured `pi` inside the command handler after a reload. It looks like using `sendUserMessage` on the captured `pi` might be stale. I need to use `ctx.sendUserMessage` instead, since the command handler has access to `ctx`. Alright, I need to adjust `runHumandSkillCommand` to make sure it uses `ctx.sendUserMessage` to avoid this issue. Let's get this fixed!

edit

const runHumandSkillCommand = async (skillName: HumandCommandSkill, args: string, ctx: any) => {


Validation failed for tool "edit":
  - edits.1.newText: must have required properties newText
  - edits.1: must not have additional properties

Received arguments:
{
  "path": "/home/sebas/pi-config/extensions/humand-mode.ts",
  "edits": [
    {
      "oldText": "\tconst runHumandSkillCommand = async (skillName: HumandCommandSkill, args: string, ctx: any) => {\n",
      "newText": "\tconst runHumandSkillCommand = async (skillName: HumandCommandSkill, args: string, ctx: any) => {\n"
    },
    {
      "oldText": "\t\tpi.sendUserMessage(request);\n",
      "\t\tctx.sendUserMessage(request);\n": ""
    }
  ]
}


Successfully replaced 1 block(s) in /home/sebas/pi-config/extensions/humand-mode.ts.

     ...
  64 			`Load and follow the active Humand skill named \"${skillName}\" from the session resources.`,
  65 			trimmed ? `Original command arguments: ${trimmed}` : "Original command arguments: (none)",
  66 			"Execute the command workflow now.",
  67 		].join("\n");
- 68 		pi.sendUserMessage(request);
+ 68 		ctx.sendUserMessage(request);
  69 	};
  70
  71 	pi.on("resources_discover", async (event) => {
  72 		if (!detectHumandMode(event.cwd)) return undefined;
     ...

--- /home/sebas/pi-config/extensions/humand-mode.ts
+++ /home/sebas/pi-config/extensions/humand-mode.ts
@@ -64,9 +64,9 @@
 			`Load and follow the active Humand skill named \"${skillName}\" from the session resources.`,
 			trimmed ? `Original command arguments: ${trimmed}` : "Original command arguments: (none)",
 			"Execute the command workflow now.",
 		].join("\n");
-		pi.sendUserMessage(request);
+		ctx.sendUserMessage(request);
 	};

 	pi.on("resources_discover", async (event) => {
 		if (!detectHumandMode(event.cwd)) return undefined;


Extension error (command:status): ctx.sendUserMessage is not a function


bash

cd /home/sebas/pi-config && rg -n "sendUserMessage\(|sendMessage\(" extensions node_modules/@mariozechner/pi-coding-agent/dist -g '!*.map' | head -80


extensions/humand-mode.ts:68:		ctx.sendUserMessage(request);
node_modules/@mariozechner/pi-coding-agent/dist/core/agent-session.js:696:            // Extension commands manage their own LLM interaction via pi.sendMessage()
node_modules/@mariozechner/pi-coding-agent/dist/core/agent-session.js:995:    async sendUserMessage(content, options) {
node_modules/@mariozechner/pi-coding-agent/dist/core/agent-session.js:1717:                this.sendUserMessage(content, options).catch((err) => {
node_modules/@mariozechner/pi-coding-agent/dist/core/agent-session.js:2505:        context.sendUserMessage = (content, options) => this.sendUserMessage(content, options);
node_modules/@mariozechner/pi-coding-agent/dist/core/extensions/types.d.ts:287:    sendUserMessage(content: string | (TextContent | ImageContent)[], options?: {
node_modules/@mariozechner/pi-coding-agent/dist/core/extensions/types.d.ts:841:    sendUserMessage(content: string | (TextContent | ImageContent)[], options?: {
node_modules/@mariozechner/pi-coding-agent/dist/core/extensions/loader.js:202:        sendMessage(message, options) {
node_modules/@mariozechner/pi-coding-agent/dist/core/extensions/loader.js:204:            runtime.sendMessage(message, options);
node_modules/@mariozechner/pi-coding-agent/dist/core/extensions/loader.js:206:        sendUserMessage(content, options) {
node_modules/@mariozechner/pi-coding-agent/dist/core/extensions/loader.js:208:            runtime.sendUserMessage(content, options);
node_modules/@mariozechner/pi-coding-agent/dist/core/agent-session.d.ts:373:    sendUserMessage(content: string | (TextContent | ImageContent)[], options?: {
node_modules/@mariozechner/pi-coding-agent/dist/core/messages.d.ts:29: * Message type for extension-injected messages via sendMessage().


 */
export interface ExtensionCommandContext extends ExtensionContext {
    /** Wait for the agent to finish streaming */
    waitForIdle(): Promise<void>;
    /** Start a new session, optionally with initialization. */
    newSession(options?: {
        parentSession?: string;
        setup?: (sessionManager: SessionManager) => Promise<void>;
        withSession?: (ctx: ReplacedSessionContext) => Promise<void>;
    }): Promise<{
        cancelled: boolean;
    }>;
    /** Fork from a specific entry, creating a new session file. */
    fork(entryId: string, options?: {
        position?: "before" | "at";
        withSession?: (ctx: ReplacedSessionContext) => Promise<void>;
    }): Promise<{
        cancelled: boolean;
    }>;
    /** Navigate to a different point in the session tree. */
    navigateTree(targetId: string, options?: {
        summarize?: boolean;
        customInstructions?: string;
        replaceInstructions?: boolean;
        label?: string;
    }): Promise<{
        cancelled: boolean;
    }>;
    /** Switch to a different session file. */
    switchSession(sessionPath: string, options?: {
        withSession?: (ctx: ReplacedSessionContext) => Promise<void>;
    }): Promise<{
        cancelled: boolean;
    }>;
    /** Reload extensions, skills, prompts, and themes. */
    reload(): Promise<void>;
}
/**
 * Fresh command-capable context bound to the replacement session after a session switch.
 *
 * This is passed to `withSession()` callbacks on `newSession()`, `fork()`, and `switchSession()`.
 */
export interface ReplacedSessionContext extends ExtensionCommandContext {
    sendMessage<T = unknown>(message: Pick<CustomMessage<T>, "customType" | "content" | "display" | "details">, options?: {
        triggerTurn?: boolean;
        deliverAs?: "steer" | "followUp" | "nextTurn";
    }): Promise<void>;
    sendUserMessage(content: string | (TextContent | ImageContent)[], options?: {
        deliverAs?: "steer" | "followUp";
    }): Promise<void>;
}
/** Rendering options for tool results */
export interface ToolRenderResultOptions {
    /** Whether the result view is expanded */
    expanded: boolean;
    /** Whether this is a partial/streaming result */
    isPartial: boolean;
}
/** Context passed to tool renderers. */
export interface ToolRenderContext<TState = any, TArgs = any> {
    /** Current tool call arguments. Shared across call/result renders for the same tool call. */
    args: TArgs;
    /** Unique id for this tool execution. Stable across call/result renders for the same tool call. */
    toolCallId: string;
    /** Invalidate just this tool execution component for redraw. */
    invalidate: () => void;
    /** Previously returned component for this render slot, if any. */
    lastComponent: Component | undefined;
    /** Shared renderer state for this tool row. Initialized by tool-execution.ts. */
    state: TState;

[864 more lines in file. Use offset=310 to continue.]


    // =========================================================================
    /**
     * Send a prompt to the agent.
     * - Handles extension commands (registered via pi.registerCommand) immediately, even during streaming
     * - Expands file-based prompt templates by default
     * - During streaming, queues via steer() or followUp() based on streamingBehavior option
     * - Validates model and API key before sending (when not streaming)
     * @throws Error if streaming and no streamingBehavior specified
     * @throws Error if no model selected or no API key available (when not streaming)
     */
    async prompt(text, options) {
        const expandPromptTemplates = options?.expandPromptTemplates ?? true;
        const preflightResult = options?.preflightResult;
        let messages;
        try {
            // Handle extension commands first (execute immediately, even during streaming)
            // Extension commands manage their own LLM interaction via pi.sendMessage()
            if (expandPromptTemplates && text.startsWith("/")) {
                const handled = await this._tryExecuteExtensionCommand(text);
                if (handled) {
                    // Extension command executed, no prompt to send
                    preflightResult?.(true);
                    return;
                }
            }
            // Emit input event for extension interception (before skill/template expansion)
            let currentText = text;
            let currentImages = options?.images;
            if (this._extensionRunner.hasHandlers("input")) {
                const inputResult = await this._extensionRunner.emitInput(currentText, currentImages, options?.source ?? "interactive");

[1812 more lines in file. Use offset=710 to continue.]


cd /home/sebas/pi-config && rg -n "This extension ctx is stale" node_modules/@mariozechner/pi-coding-agent/dist/core/extensions -g '!*.map'


node_modules/@mariozechner/pi-coding-agent/dist/core/extensions/runner.js:289:    invalidate(message = "This extension ctx is stale after session replacement or reload. Do not use a captured pi or command ctx after ctx.newSession(), ctx.fork(), ctx.switchSession(), or ctx.reload(). For newSession, fork, and switchSession, move post-replacement work into withSession and use the ctx passed to withSession. For reload, do not use the old ctx after await ctx.reload().") {
node_modules/@mariozechner/pi-coding-agent/dist/core/extensions/loader.js:136:                    "This extension ctx is stale after session replacement or reload. Do not use a captured pi or command ctx after ctx.newSession(), ctx.fork(), ctx.switchSession(), or ctx.reload(). For newSession, fork, and switchSession, move post-replacement work into withSession and use the ctx passed to withSession. For reload, do not use the old ctx after await ctx.reload().";


        setLabel: notInitialized,
        getActiveTools: notInitialized,
        getAllTools: notInitialized,
        setActiveTools: notInitialized,
        // registerTool() is valid during extension load; refresh is only needed post-bind.
        refreshTools: () => { },
        getCommands: notInitialized,
        setModel: () => Promise.reject(new Error("Extension runtime not initialized")),
        getThinkingLevel: notInitialized,
        setThinkingLevel: notInitialized,
        flagValues: new Map(),
        pendingProviderRegistrations: [],
        assertActive,
        invalidate: (message) => {
            state.staleMessage ??=
                message ??
                    "This extension ctx is stale after session replacement or reload. Do not use a captured pi or command ctx after ctx.newSession(), ctx.fork(), ctx.switchSession(), or ctx.reload(). For newSession, fork, and switchSession, move post-replacement work into withSession and use the ctx passed to withSession. For reload, do not use the old ctx after await ctx.reload().";
        },
        // Pre-bind: queue registrations so bindCore() can flush them once the
        // model registry is available. bindCore() replaces both with direct calls.
        registerProvider: (name, config, extensionPath = "<unknown>") => {
            runtime.pendingProviderRegistrations.push({ name, config, extensionPath });
        },
        unregisterProvider: (name) => {
            runtime.pendingProviderRegistrations = runtime.pendingProviderRegistrations.filter((r) => r.name !== name);
        },
    };
    return runtime;
}
/**
 * Create the ExtensionAPI for an extension.
 * Registration methods write to the extension object.
 * Action methods delegate to the shared runtime.
 */
function createExtensionAPI(extension, runtime, cwd, eventBus) {
    const api = {
        // Registration methods - write to extension
        on(event, handler) {
            runtime.assertActive();
            const list = extension.handlers.get(event) ?? [];
            list.push(handler);
            extension.handlers.set(event, list);
        },
        registerTool(tool) {
            runtime.assertActive();
            extension.tools.set(tool.name, {
                definition: tool,
                sourceInfo: extension.sourceInfo,
            });
            runtime.refreshTools();
        },
        registerCommand(name, options) {
            runtime.assertActive();
            extension.commands.set(name, {
                name,
                sourceInfo: extension.sourceInfo,
                ...options,
            });
        },
        registerShortcut(shortcut, options) {
            runtime.assertActive();
            extension.shortcuts.set(shortcut, { shortcut, extensionPath: extension.path, ...options });
        },
        registerFlag(name, options) {
            runtime.assertActive();
            extension.flags.set(name, { name, extensionPath: extension.path, ...options });
            if (options.default !== undefined && !runtime.flagValues.has(name)) {
                runtime.flagValues.set(name, options.default);
            }
        },
        registerMessageRenderer(customType, renderer) {
            runtime.assertActive();
            extension.messageRenderers.set(customType, renderer);
        },
        // Flag access - checks extension registered it, reads from runtime
        getFlag(name) {
            runtime.assertActive();
            if (!extension.flags.has(name))
                return undefined;
            return runtime.flagValues.get(name);
        },
        // Action methods - delegate to shared runtime
        sendMessage(message, options) {
            runtime.assertActive();
            runtime.sendMessage(message, options);
        },
        sendUserMessage(content, options) {
            runtime.assertActive();
            runtime.sendUserMessage(content, options);
        },
        appendEntry(customType, data) {
            runtime.assertActive();
            runtime.appendEntry(customType, data);
        },
        setSessionName(name) {
            runtime.assertActive();
            runtime.setSessionName(name);
        },
        getSessionName() {
            runtime.assertActive();
            return runtime.getSessionName();
        },
        setLabel(entryId, label) {
            runtime.assertActive();
            runtime.setLabel(entryId, label);
        },
        exec(command, args, options) {
            runtime.assertActive();
            return execCommand(command, args, options?.cwd ?? cwd, options);
        },

[254 more lines in file. Use offset=230 to continue.]


     ...
  64 			`Load and follow the active Humand skill named \"${skillName}\" from the session resources.`,
  65 			trimmed ? `Original command arguments: ${trimmed}` : "Original command arguments: (none)",
  66 			"Execute the command workflow now.",
  67 		].join("\n");
- 68 		ctx.sendUserMessage(request);
+ 68 		pi.sendUserMessage(request);
  69 	};
  70
  71 	pi.on("resources_discover", async (event) => {
  72 		if (!detectHumandMode(event.cwd)) return undefined;
     ...

--- /home/sebas/pi-config/extensions/humand-mode.ts
+++ /home/sebas/pi-config/extensions/humand-mode.ts
@@ -64,9 +64,9 @@
 			`Load and follow the active Humand skill named \"${skillName}\" from the session resources.`,
 			trimmed ? `Original command arguments: ${trimmed}` : "Original command arguments: (none)",
 			"Execute the command workflow now.",
 		].join("\n");
-		ctx.sendUserMessage(request);
+		pi.sendUserMessage(request);
 	};

 	pi.on("resources_discover", async (event) => {
 		if (!detectHumandMode(event.cwd)) return undefined;


edit

## Validation checklist

- [x] `pi` starts with Humand extension enabled
- [x] Humand bootstrap vars resolve
- [x] Humand tools answer in Humand repos
- [x] Humand skill path is injected in Humand repos
- [x] Commands `/start`, `/status`, `/pr`, `/merge-watch`, `/summary` register in Pi
- [ ] Interactive session-start notice visually confirmed in TUI
- [ ] End-to-end behavior of every upstream workflow validated against external deps

## Validation checklist

- [x] `pi` starts with Humand extension enabled
- [x] Humand bootstrap vars resolve
- [x] Humand tools answer in Humand repos
- [x] Humand skill path is injected in Humand repos
- [x] Skills `skill:start`, `skill:status`, `skill:pr`, `skill:merge-watch`, `skill:summary` appear in session command inventory
- [x] Commands `/start`, `/status`, `/pr`, `/merge-watch`, `/summary` register in Pi command inventory
- [ ] Interactive session-start notice visually confirmed in TUI
- [ ] End-to-end behavior of every upstream workflow validated against external deps

### Validation evidence

Validated in `/home/sebas/work/humand/humand-web`:

- `humand_context` returned `active=true`
- `humand_status` returned configured/existing repo coverage
- bootstrap env exposed Humand config/state/local-config vars
- RPC `get_commands` showed:
  - `start`
  - `status`
  - `pr`
  - `merge-watch`
  - `summary`
  - `skill:start`
  - `skill:status`
  - `skill:pr`
  - `skill:merge-watch`
  - `skill:summary`


Successfully replaced 1 block(s) in /home/sebas/pi-config/humand/PORTING-STATUS.md.

     ...
  73 - [x] `pi` starts with Humand extension enabled
  74 - [x] Humand bootstrap vars resolve
  75 - [x] Humand tools answer in Humand repos
  76 - [x] Humand skill path is injected in Humand repos
- 77 - [x] Commands `/start`, `/status`, `/pr`, `/merge-watch`, `/summary` register in Pi
+ 77 - [x] Skills `skill:start`, `skill:status`, `skill:pr`, `skill:merge-watch`, `skill:summary` appear in session command inventory
+ 78 - [x] Commands `/start`, `/status`, `/pr`, `/merge-watch`, `/summary` register in Pi command inventory
  78 - [ ] Interactive session-start notice visually confirmed in TUI
  79 - [ ] End-to-end behavior of every upstream workflow validated against external deps
  80
+ 82 ### Validation evidence
+ 83
+ 84 Validated in `/home/sebas/work/humand/humand-web`:
+ 85
+ 86 - `humand_context` returned `active=true`
+ 87 - `humand_status` returned configured/existing repo coverage
+ 88 - bootstrap env exposed Humand config/state/local-config vars
+ 89 - RPC `get_commands` showed:
+ 90   - `start`
+ 91   - `status`
+ 92   - `pr`
+ 93   - `merge-watch`
+ 94   - `summary`
+ 95   - `skill:start`
+ 96   - `skill:status`
+ 97   - `skill:pr`
+ 98   - `skill:merge-watch`
+ 99   - `skill:summary`
+100
  81 ## Notes
  82
  83 - Do not place helper modules as top-level files in `extensions/` unless they export a real Pi extension factory. Pi auto-loads top-level extension files.
  84 - Keep Humand-specific runtime surface gated by Humand detection so Ballbox/personal sessions stay unchanged in practice.

--- /home/sebas/pi-config/humand/PORTING-STATUS.md
+++ /home/sebas/pi-config/humand/PORTING-STATUS.md
@@ -73,12 +73,32 @@
 - [x] `pi` starts with Humand extension enabled
 - [x] Humand bootstrap vars resolve
 - [x] Humand tools answer in Humand repos
 - [x] Humand skill path is injected in Humand repos
-- [x] Commands `/start`, `/status`, `/pr`, `/merge-watch`, `/summary` register in Pi
+- [x] Skills `skill:start`, `skill:status`, `skill:pr`, `skill:merge-watch`, `skill:summary` appear in session command inventory
+- [x] Commands `/start`, `/status`, `/pr`, `/merge-watch`, `/summary` register in Pi command inventory
 - [ ] Interactive session-start notice visually confirmed in TUI
 - [ ] End-to-end behavior of every upstream workflow validated against external deps

+### Validation evidence
+
+Validated in `/home/sebas/work/humand/humand-web`:
+
+- `humand_context` returned `active=true`
+- `humand_status` returned configured/existing repo coverage
+- bootstrap env exposed Humand config/state/local-config vars
+- RPC `get_commands` showed:
+  - `start`
+  - `status`
+  - `pr`
+  - `merge-watch`
+  - `summary`
+  - `skill:start`
+  - `skill:status`
+  - `skill:pr`
+  - `skill:merge-watch`
+  - `skill:summary`
+
 ## Notes

 - Do not place helper modules as top-level files in `extensions/` unless they export a real Pi extension factory. Pi auto-loads top-level extension files.
 - Keep Humand-specific runtime surface gated by Humand detection so Ballbox/personal sessions stay unchanged in practice.


bash

cd /home/sebas && pi --mode text -p "say ok" --no-session --thinking off

python3 - <<'PY'
import json, subprocess
cwd='/home/sebas/work/humand/humand-web'
p=subprocess.Popen(['pi','--mode','rpc','--no-session'],cwd=cwd,stdin=subprocess.PIPE,stdout=subprocess.PIPE,stderr=subprocess.PIPE,text=True)
p.stdin.write(json.dumps({'id':'1','type':'get_commands'})+'\n')
p.stdin.flush()
want={'start','status','pr','merge-watch','summary','skill:start','skill:status','skill:pr','skill:merge-watch','skill:summary'}
found=[]
for _ in range(30):
    line=p.stdout.readline()
    if not line:
        break
    obj=json.loads(line)
    if obj.get('type')=='response' and obj.get('command')=='get_commands' and obj.get('success'):
        found=sorted([c['name'] for c in obj['data']['commands'] if c['name'] in want])
        break
print('\n'.join(found))
p.terminate()
try:
    p.wait(timeout=3)
except Exception:
    p.kill()
PY

cd /home/sebas/work/humand/humand-web && pi --mode text -p "Print HUMAND_PI_CONFIG_HOME and HUMAND_PI_LOCAL_CONFIG_FILE only." --no-session --thinking off


ok


merge-watch
pr
skill:merge-watch
skill:pr
skill:start
skill:status
skill:summary
start
status
summary


HUMAND_PI_CONFIG_HOME=/home/sebas/.config/pi-humand
HUMAND_PI_LOCAL_CONFIG_FILE=/home/sebas/.config/pi-humand/local-config.json


**Summarizing documentation and commands**

I need to create a final concise document, ensuring it mentions the commands and skills I've loaded. I'll validate that everything is working as intended. However, it’s important to note that this may not be an end-to-end solution, so I want to make sure that’s clear. It’s about providing a complete picture while keeping expectations realistic. Okay, let’s wrap this up and do it right!

todo

Created humand/PORTING-STATUS.md, exposed upstream humand-tech skills via resources_discover skillPaths, registered /start /status /pr /merge-watch /summary commands in humand-mode.ts, and validated load in humand-web via RPC get_commands plus env/tool checks. Updated doc with validation evidence and remaining gaps.


Updated #2 (in_progress → completed)

Debug humand extension loading and restore working pi startup

Root cause: pi auto-loads top-level files in extensions/. humand-shared.ts was a helper, not an extension, so pi tried to load it as an extension and failed startup. Fixed by moving shared helper to extensions/_shared/humand-shared.ts, updating humand-mode import, restoring humand-mode.ts, and removing the top-level humand-shared.ts loader conflict. Validated pi starts in /home/sebas and /home/sebas/work/humand.

Port remaining Humand commands and document status


Sí. Hecho.

Cambios:
- doc nuevo:
  - `/home/sebas/pi-config/humand/PORTING-STATUS.md`
- extensión Humand:
  - ahora expone skills upstream desde:
    - `/home/sebas/work/humand/hu-ai-agent-plugin/plugins/humand-tech/skills`
- comandos registrados:
  - `/start`
  - `/status`
  - `/pr`
  - `/merge-watch`
  - `/summary`
- hook aclarado como referencia no runtime:
  - `/home/sebas/pi-config/humand/hooks/session-start`

Validado que cargan:
- `pi` arranca OK
- vars Humand cargan:
  - `HUMAND_PI_CONFIG_HOME`
  - `HUMAND_PI_LOCAL_CONFIG_FILE`
- en `~/work/humand/humand-web` aparecen en inventario:
  - `start`
  - `status`
  - `pr`
  - `merge-watch`
  - `summary`
  - `skill:start`
  - `skill:status`
  - `skill:pr`
  - `skill:merge-watch`
  - `skill:summary`

Ojo:
- validé carga/registro
- no validé end-to-end completo de cada workflow contra Jira/GitHub/Slack/CI real
- eso quedó marcado en el doc como pendiente

Si querés, próximo paso:
- hago pasada 2: validar `/status`, `/pr`, `/merge-watch` con casos reales y cerrar más del port.