import { registerDiscoveryHub, configureDiscoveryHub, resetDiscoveryHub, discoveryHubEnabled, discoveryManagedReadbackAllowed, prepareDiscoveryNative } from './file-discovery/hub.ts'; import { registerAgenticHub, configureAgenticHub, agenticHubEnabled, agenticHubRuntime, resetAgenticHub } from './agentic-hub.ts'; import { loadSystem1Snapshot } from "../lib/system1/config-loader.js"; import { normalizeSystem1Config } from "../lib/system1/config-v2.js"; import { registerDispatchTriage } from "./tools/dispatch-triage.ts"; import { createTriageRuntime } from "./dispatch-triage-runtime.ts"; import { type TriageConfig, type TriageInput } from "./dispatch-triage-contract.ts"; import { loadTaskTriageConfig } from "./task-triage-config.ts"; import { TASK_TRIAGE_MODEL } from "./task-triage-contract.ts"; import type { JevTransport } from "../lib/system1/jev.ts"; import { createTaskTriageRuntime, evaluatedTaskTriageInputChanged, TASK_TRIAGE_RUNTIME_ENTRY } from "./task-triage-runtime.ts"; import { adoptTaskTriageState, applyTaskTriageAdditions, recoverTaskTriageProcessState } from "./task-triage-obligations.ts"; import { confirmTaskTriageAction, confirmTaskTriageWaiver, consumeActionGrant, taskTriageActionAuditRecord, type TaskTriageActionObservation } from "./task-triage-authorization.ts"; import { registerTaskTriageRecover } from "./commands/task-triage-recover.ts"; import { registerTaskTriageWaiver } from "./commands/task-triage-waive.ts"; import { createCommunicationStore } from "./system1-communication-store.ts"; import { openSystem1Communication } from "./ui/system1-communication.ts"; import { registerRetry } from "./commands/retry.ts"; import { createHash } from "node:crypto"; import { loadProactiveConfig, isCaptureEnabled } from "./proactive-config.ts"; import { createProactiveRuntime, createHubCapture, composeHubProactive } from "./proactive-runtime.ts"; import type { ProactiveConfig } from "./proactive-types.ts"; import { registerRecover, runRecoverCommand } from "./commands/recover.ts"; import { createNoProgressGuard } from "./no-progress.ts"; import { renderNextInvocation, renderRecoverCommands } from './recover-policy.ts'; import { closeEvidenceSession, pruneEvidenceSessions } from "./execution-evidence.ts"; import { hasExplicitDispatcher, isCompleteProfile, dispatcherSelection, type ModelProfiles } from './config/model-profiles.ts'; import { createProfileActivation } from './policy/profile-activation.ts'; import { readActiveProfile, profileWorkInFlight, withProfileWork, assertProfileModel, profilePeerGate } from './policy/profile-runtime.ts'; import { resolveAssist } from './assist-profile.ts'; import { registerFilesystemTool, FILESYSTEM_POLICY_ROOTS_ENV } from './filesystem-tool.ts'; /** Agent Hub composition root: constructs mutable state, contexts, registrars, and ordered lifecycle ports. */ import type { AgentDef, AgentState, ResearchState } from "./types.ts"; import { DEFAULT_OVERRIDES, THINKING_LEVELS, parseAgentTeamOverrides, type AgentTeamOverrides } from "./config/overrides.ts"; import { loadAgentConfiguration } from "./config/agents.ts"; import { abbrevThinking, displayName, extractAskUserQuestions, extractNeedsResearch, resolveDelegateExtension, resolveThinkingLevel } from "./presentation.ts"; import { MAX_LIVE_ENTRY_CHARS, appendTimelineEvent, appendTimelineText, flushTimelineStore } from "./timeline.ts"; import type { ExtensionAPI, ExtensionContext, Theme } from "@mariozechner/pi-coding-agent"; import { DynamicBorder, getMarkdownTheme as getPiMdTheme, copyToClipboard } from "@mariozechner/pi-coding-agent"; import { Text, Box, Container, Spacer, Markdown, matchesKey, Key, type AutocompleteItem, truncateToWidth, visibleWidth, } from "@mariozechner/pi-tui"; import { spawn, type ChildProcess } from "child_process"; import { spawnPiAgent, spawnPiAgentWithModelFallback, killPiTree, type Termination } from "./spawn.ts"; import { fenceOperatorCancel } from "./drift-runtime.ts"; import { researchTerminationOutcome, researchWatchdogSpawnOptions } from "./research-watchdog.ts"; import { composeFleetFooterHint, renderHubFooterLeft } from "./footer.ts"; import { HARNESS_VERSION, registerVersionStatus } from "./version.ts"; import { cancelLocalOwnedProcess, cancelLocalWaitOnly, monitorKeyForAgent } from "./monitor-control.ts"; import { createMonitorLifecycle } from "./monitor-lifecycle.ts"; import { createMonitorSessionBridge } from "./monitor-session-bridge.ts"; import { AGENT_ID_ENV, ASK_ENDPOINT_ENV, EXEMPTIONS_FILE_ENV, exemptionsFilePath, type AccessRequest, } from "../lib/damage-control-shared.ts"; import { applyModelOverride, clampDelegateDepth, fallbackModelFor, isReadOnlyToolList, MAX_DELEGATE_DEPTH, normalizeAgentInput, orchestratorNeedsRoster, parseTeamsYaml, safeAgentKey, safePathWithin, taskFingerprint, upsertTeamInYaml } from "./helpers.ts"; import { DEFAULT_TASK_TIER, addTaskClockWait, applyTierChange, blockingFindingCap, checkReviewRoundCap, checkTaskBudget, checkTierPersonaGate, checkTurnBudget, createTaskClock, isReviewPersona, openTaskClock, remainingTaskResearch, reviewBudgetClause, reviewRoundCap } from "./run-budget.js"; import { countReviewFindings, findingBudgetNotice } from "./review-findings.js"; import { checkDocsLane, docsLaneNotice } from "./docs-lane.js"; import { checkExternalBlockerGate, extractExternalBlockers } from "./external-blocker.js"; import { DEFAULT_RUN_HISTORY_KEEP, normalizeRunHistoryKeep } from "./run-namespace.js"; import { validateAssertionBatch } from "./assertion-ledger.js"; import { DEFAULT_PROVIDER_LIMITS, createProviderSemaphore, parseProviderLimits } from "./provider-semaphore.js"; import { PANE_PROMPT_TIMEOUT_MS, launchPeerInPane, peerReadyVerdict, unaddressedPeerSweep, } from "../lib/spawned-peers.js"; import { contextPct, estimatePromptTokens, resolveContextWindow } from "./context-window.js"; import { DEFAULT_WATCHDOG_SETTING, WATCHDOG_SETTINGS, normalizeWatchdogSetting, resolveWatchdogActive } from "./drift-watchdog.js"; import { createWatchdogActivity, createTaskTriageActivity, type WatchdogActivity, type TaskTriageActivity } from "./system1-activity.ts"; import { createWatchdogSystem1Session, disposeWatchdogSystem1Session, readWatchdogSystem1Snapshot, type WatchdogSystem1Session } from "./system1-runtime.ts"; import { shouldExtractReturn } from "./return-extract.js"; import { crossCheck, deliveryDisposition, extractAssertionIds, parseDeliveredReturn } from "./return-contract.js"; import { checkScope, diffAgainst, snapshotWorktree, worktreeRevision } from "./scope-gate.js"; import { validateEvidence } from "./evidence-rules.js"; import { comsRequiredRefusal, explicitComsRefusal, parseDispatchPolicy, resolveDispatchBackend } from "./backend-policy.js"; import { NATIVE_ROSTER_ENTRY_TYPE, persistedNativeRosterState, resolveSessionWorkMode, resolveSessionRoster, type WorkMode } from "./work-mode.ts"; import { compactWorkMode } from "./work-mode-controls.ts"; import { registerWorkMode } from "./commands/work-mode.ts"; import { registerAgentsTeam } from "./commands/agents-team.ts"; import { registerAgentsList } from "./commands/agents-list.ts"; import { registerAgentsHistory } from "./commands/agents-history.ts"; import { registerAgentsAdd } from "./commands/agents-add.ts"; import { registerAgentsDrop } from "./commands/agents-drop.ts"; import { registerAgentsSave } from "./commands/agents-save.ts"; import { registerAgentsKill } from "./commands/agents-kill.ts"; import { registerAgentsRestart } from "./commands/agents-restart.ts"; import { registerContextCommand } from "./commands/context-command.ts"; import { registerAudit } from "./commands/audit.ts"; import { showSessionAudit } from "./session-audit.ts"; import { buildWatchdogReport, buildProactiveReport, commandProactiveLabels, readProactiveReport, formatWatchdogStatus, readWatchdogEvents } from "./system1-report.ts"; import { createProcessState, processAllowsPersona, processOpenObligations, evaluateProcessObligations, latestProcessState, processAuditRecord, processTaskIdentityConflicts, processPreEffectGate, missingProcessRoleRecoveryHints, activeAdditionRecoveryHints, taskTransitionRecoveryHint, transitionToolDecision, latestPendingTaskTransition, TASK_TRANSITION_ENTRY_TYPE, type ProcessObligationState, type ProcessVerdict } from "./process-obligations.ts"; import { registerHubReport } from "./commands/hub-report.ts"; import { registerZoom } from "./commands/zoom.ts"; import { registerDispatchPolicy } from "./commands/dispatch-policy.ts"; import { registerAgentModel } from "./commands/agent-model.ts"; import { registerAgentModelThinking } from "./commands/agent-model-thinking.ts"; import { registerModels } from "./commands/models.ts"; import { registerAgentModelsSubstitute } from "./commands/agent-models-substitute.ts"; import { registerWatchdog } from "./commands/watchdog.ts"; import { registerComs } from "./commands/coms.ts"; import { registerHandoff } from "./commands/handoff.ts"; import { registerCompound } from "./commands/compound.ts"; import { registerPoll } from "./commands/poll.ts"; import { registerDebate } from "./commands/debate.ts"; import type { CommandContext } from "./commands/context.ts"; import { formatAfPollStarted, formatAfPollVoiceProgress, handleAfPoll } from "./poll-command.ts"; import { formatAfDebateStarted, formatAfDebateVoiceProgress, handleAfDebate } from "./debate-command.ts"; import { registerDispatchAgent } from "./tools/dispatch-agent.ts"; import { registerSpawnResearch } from "./tools/spawn-research.ts"; import { registerRunFlow } from "./tools/run-flow.ts"; import { resolvePersona as resolveFlowPersona } from "../../agent-fleet/scripts/workflows/lib/personas.ts"; import { assertProfileModel as assertFlowProfileModel } from "../../agent-fleet/scripts/workflows/lib/model-profile.ts"; import { registerSetTaskTier } from "./tools/set-task-tier.ts"; import { registerTeamAdjust } from "./tools/team-adjust.ts"; import { registerVerificationContract } from "./tools/verification-contract.ts"; import { registerComsTools } from "./tools/coms-tools.ts"; import { paneTail, peerManifest, peerPersonaExists, registerFleetTools, spawnDelaySeconds, STAGGER_ENV_VAR, waitForPeerRegistration } from "./tools/fleet-tools.ts"; import type { ToolContext } from "./tools/context.ts"; import { createToolExecutionOrchestration } from "./tools/execution-orchestration.ts"; import { latestPersistedCapabilityState, type ContextState, type PendingOperation } from "./capability-packs.ts"; import { contextPressureDiagnostic, createContextPressureState, transitionContextPressure, type ContextPressureState } from "./context-pressure.ts"; import { confirmationOutcome, capabilityConfirmationPack, capabilityConfirmationQuestion, type ConfirmableCapabilityPack } from "./capability-confirmation.ts"; import { confirmRecoverAction, confirmTaskSupersession, createBudgetRecovery, createReservedTaskIdentityReset } from "./budget-recovery.ts"; import { requestRuntimeAsk } from "../ask-user-remote/runtime-ask.ts"; import { registerBudgetContinue } from "./commands/budget-continue.ts"; import { observeAskUserResults } from "../ask-user-remote/index.ts"; import { handleQuestionEnvelope, registerQuestionPeer } from "../ask-user-remote/questions.ts"; import { buildHubPeerSpawnPlan, launchHubPeerInPane } from "./peer-spawn-plan.ts"; import { RESEARCH_TOOLS, createResearchRuntime, parseResearchHandle } from "./research/runtime.ts"; import { requireSafetyHarness, resolveSafetyHarness } from "./safety-routing.ts"; import { createAccessApprovalRouter } from "./access-approval.ts"; import { readdirSync, readFileSync, existsSync, mkdirSync, unlinkSync, writeFileSync, rmSync } from "fs"; import { herdrPaneId } from "../lib/herdr-presence.ts"; import { herdr as herdrApi, herdrAvailable } from "../lib/herdr-client.ts"; import { abbreviateModel, createComsPeer, hexFg, nowIso, readAllRegistryEntries, readAllRegistryEntriesAcrossProjects, type ComsIdentity, type RegistryEntry as ComsRegistryEntry, TIMEOUT_MS, } from "../lib/coms-core.ts"; import { createGridUI } from "./ui/grid.ts"; import { createFleetSource } from "./ui/fleet-source.ts"; import { buildFleetRows } from "../lib/fleet-read-model.ts"; import { createFleetActions } from "./ui/fleet-actions.ts"; import { createDetailPanel } from "./ui/detail-panel.ts"; import { createFleetDashboard } from "./ui/fleet-dashboard.ts"; import { createContextBudgetUi } from "./ui/context-budget.ts"; import { createPoolPresentation } from "./ui/pool.ts"; import { registerInputShortcuts } from "./input/shortcuts.ts"; import { createCompletionPresentation } from "./input/completions.ts"; import { createResearchControls } from "./research/controls.ts"; import { createContextPressureLifecycle, createContextPressureRootState } from "./lifecycle/context-pressure.ts"; import { createTurnLifecycleHandlers, registerTurnPresence } from "./lifecycle/turn-handlers.ts"; import { createMonitorSession } from "./lifecycle/monitor-session.ts"; import { applySessionOverrides, registerSessionOrchestration, resetHubSession } from "./lifecycle/session-orchestration.ts"; import { openZoom, type TimelineEntry, type Zoomable } from "./ui/zoom.ts"; import { openHistory } from "./ui/history.ts"; import { createExecutionHistoryStore, type HistoryEntry } from "./ui/history-store.ts"; import { createDispatchComs, createDispatchNative, createDispatchObservability, type DelegationChild } from "./dispatch-core.ts"; import type { FleetRow } from "../lib/fleet-read-model.ts"; import { gridColumnsForSize } from "../lib/fleet-dashboard-ops.ts"; import { createFleetTranscriptStore } from "../lib/fleet-transcript-store.ts"; import type { ContextBudgetComponent } from "../lib/context-budget.ts"; import { buildHubSystemPrompt as assembleHubPrompt } from "./prompts/system-prompt.ts"; import type { HubPromptContext } from "./prompts/context.ts"; import { buildSessionStartNotice, createSessionFooter } from "./prompts/session-start.ts"; import { registerSessionStart } from "./session-start.ts"; import { createHubStateContext } from "./context/hub-state.ts"; import { createAgentStateFactory } from "./context/agent-state.ts"; import { createBudgetContext, createSessionTotals, freshTurnReport, type TurnReport } from "./context/budgets.ts"; import { createAssertionsArtifactsContext, type Assertion, type InputArtifactPreview } from "./context/assertions-artifacts.ts"; import { createModelPolicy } from "./policy/models.ts"; import { createRosterPolicy } from "./policy/roster.ts"; import { createWorkModePolicy } from "./policy/work-mode.ts"; import { catalogSnapshot, latestPersistedToolCatalog, toolCatalogNotice, TOOL_CATALOG_ENTRY_TYPE, type ToolCatalogDelta } from "./tool-catalog-state.ts"; import { createToolCatalogRuntime } from "./tool-catalog-runtime.ts"; import { createUnknownToolCounter, latestPersistedUnknownToolCounter, observeUnknownToolCalls, restoreUnknownToolCounter, unknownToolNotice, UNKNOWN_TOOL_COUNTER_ENTRY_TYPE } from "./unknown-tool-counter.ts"; import { buildProjectDocsProtocol, buildProjectRulesProtocol, nativeResearchSystemPrompt } from "../lib/context-budget-child-prompt.ts"; import { parseEnvFile, resolveEnvFilePath } from "../../agent-fleet/scripts/lib/herdr-layout.ts"; import { worktreeTag } from "../../agent-fleet/scripts/lib/team-project.ts"; import { join, resolve } from "path"; import * as fs from "node:fs"; import * as path from "node:path"; import * as crypto from "node:crypto"; import { fileURLToPath } from "node:url"; // ── Extension ──────────────────────────────────── const CONTEXT_WARN_THRESHOLD = 70; const RESEARCHER_PERSONAS = new Set(["researcher", "deep-researcher"]); /** * Test-only transport seam for real-Pi acceptance. With the offline guard, * the normal shared System 1 service uses deterministic wire responses rather * than HTTPS; config, readiness, adapter, deadlines and consumers remain real. * A profile alone cannot activate this seam outside the guarded test process. */ let taskTriageFakeInstances = 0; function resolveTaskTriageTestTransport(): JevTransport | undefined { const profile = process.env.AGENT_HUB_TASK_TRIAGE_FAKE; const guard = `--import=${fileURLToPath(new URL("../../../bin/test/helpers/system1-no-network.js", import.meta.url))}`; if (!profile || process.env.PI_OFFLINE !== "1" || !process.env.NODE_OPTIONS?.split(/\s+/).includes(guard)) return undefined; const hot = profile === "security" ? "security_change" : profile === "wide" ? "wide_change" : profile === "irreversible" ? "irreversible_execution" : null; if (!hot && profile !== "plain" && profile !== "timeout") return undefined; const instanceId = ++taskTriageFakeInstances; return async request => { const wire = JSON.parse(request.body.toString("utf8")); const recordPath = process.env.AGENT_HUB_TASK_TRIAGE_FAKE_RECORD; if (recordPath) { try { fs.appendFileSync(recordPath, JSON.stringify({ at: Date.now(), profile, instanceId, pid: process.pid, questions: Object.keys(wire.questions), stateFingerprint: createHash("sha256").update(JSON.stringify(wire.state)).digest("hex"), questionFingerprint: createHash("sha256").update(JSON.stringify(wire.questions)).digest("hex"), stack: (new Error("fake-transport").stack ?? "").split("\n").slice(1, 6) }) + "\n"); } catch { /* evidence best-effort */ } } if (profile === "timeout") { return new Promise((_, reject) => { if (request.signal.aborted) return reject(new Error("guarded fake transport deadline")); request.signal.addEventListener("abort", () => reject(new Error("guarded fake transport deadline")), { once: true }); }); } return { status: 200, headers: {}, body: JSON.stringify({ model: TASK_TRIAGE_MODEL, answers: Object.fromEntries(Object.keys(wire.questions).map(id => [id, { type: "noul", noul: id === hot ? 0.95 : 0.05 }])), usage: { input_tokens: 1, output_tokens: 1 }, }) }; }; } export default function (pi: ExtensionAPI) { // ── Embedded coms: identity CLI flags ── // Registered here (factory load time) so pi's CLI parser accepts them. Because // coms is EMBEDDED (one extension, not a second `-e`), these register exactly once. pi.registerFlag("name", { description: "Coms: override agent name (else frontmatter or auto-generated)", type: "string", default: undefined }); pi.registerFlag("purpose", { description: "Coms: override agent purpose (else frontmatter description)", type: "string", default: undefined }); pi.registerFlag("project", { description: "Coms: project namespace for peer discovery", type: "string", default: "default" }); pi.registerFlag("color", { description: "Coms: hex color #RRGGBB (else frontmatter or palette fallback)", type: "string", default: undefined }); pi.registerFlag("explicit", { description: "Coms: hide from auto-discovery; addressable only by exact name", type: "boolean", default: false }); pi.registerFlag("solo", { description: "Run without the coms layer (fixed specialists + research only — `just fleet hub --solo`)", type: "boolean", default: false }); pi.registerFlag("work-mode", { description: "Fleet main-agent work mode: operator|orchestrator", type: "string", default: undefined }); pi.registerFlag("agent-team", { description: "Internal: activate a named native roster from .pi/agents/teams.yaml", type: "string", default: undefined }); // ── Embedded coms: shared peer state ── let currentCtx: ExtensionContext | null = null; let refreshFleetUi = () => {}; const coms = createComsPeer({ pi, getContext: () => currentCtx, onPeersChanged: () => refreshFleetUi(), acceptInbound: () => currentCtx && modelWorkBlockedByRosterRecovery(currentCtx) ? "orchestrator roster recovery required" : null, handleCustomEnvelope: (socket, envelope) => { if (handleQuestionEnvelope(socket, envelope)) return true; if (envelope.type !== "access_request") return false; void accessApprovalRouter.handle(socket, envelope as AccessRequest); return true; }, }); const unregisterQuestions = registerQuestionPeer(() => coms.ready && coms.identity ? { project: coms.identity.project, peer: coms.identity.name, sessionId: coms.identity.session_id, startedAt: coms.identity.started_at, } : null); let identity: ComsIdentity | null = null; const peerCards = coms.peerCards; const pendingReplies = coms.pendingReplies; const inboundQueue = coms.inboundQueue; let comsReady = false; let monitorLifecycle: ReturnType | null = null; let monitorHubId: string | null = null; let monitorTurnId: string | null = null; let monitorBridge: ReturnType | null = null; let monitorOwnerId: string | undefined; // ── Damage-control exemptions + access escalation state ── // exemptionsFile is the session-scoped shared exemptions file: /af-allow // session grants (via the co-loaded damage-control-continue) and approved // escalations land here, and spawned children read it through the // AGENT_HUB_EXEMPTIONS_FILE env plumbing. let exemptionsFile: string | null = null; const accessApprovalRouter = createAccessApprovalRouter({ getContext: () => currentCtx, getExemptionsFile: () => exemptionsFile, appendLog: (entry) => { try { pi.appendEntry("damage-control-log", entry); } catch { /* best-effort */ } }, now: nowIso, }); const agentStates: Map = new Map(); // Live-only research helpers, keyed by numeric id (handle `rN`). Visible in the // Fleet Dashboard (Alt+A) while running; evicted from this map on every terminal // outcome. Session/transcript/findings files and /af-agents-history survive. let researchStates: Map = new Map(); // Metadata only: prompt text is never retained by this ledger. let lastHubLedger: ContextBudgetComponent[] = []; let nextResearchId = 1; // ── Execution history (/af-agents-history) ────────── // The typed store owns entries and turn/ask_user bookkeeping; index.ts owns // composition and wires lifecycle events to its explicit API. const executionHistory = createExecutionHistoryStore(); let researchPersonas: AgentDef[] = []; let allAgentDefs: AgentDef[] = []; let teams: Record = {}; // Named model profiles from .pi/agents/model-profiles.yaml (validated at // session start) and the session-lifetime per-persona model overrides set by // /af-agent-model and /af-models (lowercase persona name → pi model spec). Overrides // reset on session_start; profiles make re-applying cheap. let modelProfiles: ModelProfiles = {}; let modelProfileErrors: string[] = []; // Backend routing policy (.pi/agents/dispatch-policy.yaml): per dispatch, a // member preferring coms is served by a live same-name pool peer instead of a // native subagent spawn. Missing file → everything native (status quo). let dispatchPolicy: { default: string; grace_s: number; substitutions: Record; } = { default: "native", grace_s: 30, substitutions: {} }; let dispatchPolicyWarnings: string[] = []; // One "coms peer missing → native" notice per member per team activation. const comsMissNotified = new Set(); let activeTeamName = ""; let gridCols = 2; let widgetCtx: any; let sessionDir = ""; let contextWindow = 0; // Per-provider in-flight cap for the hub's OWN spawns (specialists and // research helpers, which the dispatcher can start in parallel). Delegate // children are gated by the same policy inside their parent's process. The // drift judge and the return extractor are deliberately NOT gated: both run // while a specialist holds a permit, so queueing them behind that specialist // would stall the very watchdog that is supposed to stop it. const providerSemaphore = createProviderSemaphore({ ...DEFAULT_PROVIDER_LIMITS, ...parseProviderLimits(process.env.AGENT_HUB_PROVIDER_LIMITS), }); /** * pi's model registry as a plain lookup — the documented source for every * model's window, and the same one the dispatcher's own window comes from. */ function modelWindowLookup(ctx: any): (provider: string, modelId: string) => any { return (provider, modelId) => ctx?.modelRegistry?.find?.(provider, modelId); } /** Say so when a run is about to wait on a provider permit, not just hang. */ function notifyProviderQueue(model: string, label: string, ctx: any): void { const cap = providerSemaphore.limitFor(model); if (cap == null || providerSemaphore.inFlight(model) < cap) return; const ahead = providerSemaphore.queued(model) + 1; ctx?.ui?.notify( `${label}: queued — ${cap} requests already in flight on this provider (${ahead} waiting). ` + "Raise or disable the cap with AGENT_HUB_PROVIDER_LIMITS (e.g. custom=4, custom=off).", "info", ); } let activeWritableDispatches = 0; let writableOverlapCounter = 0; let pendingHandoff: { target: string; token: string } | null = null; const hubStateCtx = createHubStateContext({ getCurrentContext: () => currentCtx, setCurrentContext: value => { currentCtx = value; }, getExemptionsFile: () => exemptionsFile, setExemptionsFile: value => { exemptionsFile = value; }, getSessionDir: () => sessionDir, setSessionDir: value => { sessionDir = value; }, getWidgetContext: () => widgetCtx, setWidgetContext: value => { widgetCtx = value; }, getPendingHandoff: () => pendingHandoff, setPendingHandoff: value => { pendingHandoff = value; }, }); let userLanguage: string = DEFAULT_OVERRIDES.language; // Project rule folders from the overrides file's `rules:` key (repo-relative, // validated at session_start). Non-empty → every dispatched specialist gets a // "Project rules" prompt block; personas resolve the folders index-first (a // top-level README.md/index.md is a loading manifest) with recursive discovery // as the no-index fallback. let projectRulesDirs: string[] = []; // Project documentation entry points from the overrides file's `docs:` key // (repo-relative files or folders, validated at session_start). Non-empty → // dispatched specialists and research helpers get a "Project docs" prompt block. let projectDocsPaths: string[] = []; // Prompt blocks for the project's own rules (HOW — compliance) and docs // (WHAT/WHY — orientation). Rules discovery is index-first so a curated rule // tree (README manifest + session bundles) is honored instead of bulk-read; // the blind recursive find is only the no-index fallback. The validation duty // itself is written into the planner/code-reviewer personas. function buildRulesProtocol(): string { return buildProjectRulesProtocol(projectRulesDirs); } function buildDocsProtocol(): string { return buildProjectDocsProtocol(projectDocsPaths); } /** One canonical repository context file plus explicitly configured rule roots. */ function specialistProjectPolicyPaths(cwd: string): string[] { const canonical = ["AGENTS.md", "CLAUDE.md"].find(candidate => existsSync(path.join(cwd, candidate))); return [...new Set([...(canonical ? [canonical] : []), ...projectRulesDirs])]; } // The one supported safety harness. Every native specialist, researcher, and // nested delegate receives it; a missing harness refuses child dispatch. let safetyHarnessPath: string | null = null; // Resolved once at session_start: the delegate extension injected into // specialists that declare `subagents:` (null → delegation disabled). let delegateExtPath: string | null = null; // Per-tool deadline for read/grep/find/ls in research helpers/delegates. The // whole-run bound is separate: the turn budget's agentTurnMs (run-budget.js). let reconSearchTimeoutMs: number | null = 120_000; // ── Per-turn budgets (run-budget.js) ── // Envelopes follow task tier. Override keys are a ceiling (min with the tier). // Counters reset in before_agent_start, or after an explicit one-click // continuation. executionHistory.turnStartedAt() is the active-time base; ask_user // waits are subtracted. let budgetOverrides: AgentTeamOverrides["budgetOverrides"] = {}; let turnDispatchCount = 0; let turnResearchCount = 0; // Human wait time is not fleet work. Track it separately from history UI // intervals so a continuation can rebase the budget without erasing history. let turnBudgetAskUserWaitMs = 0; let taskContinuationCount = 0; let turnContinuationCount = 0; // ── Task-scoped budget & tier (run-budget.js) ── // A per-message allowance cannot bound a task: every steering message opened a // fresh turn window, so a run could spend 8 dispatches and 60 minutes again and // again without any counter ever binding. These counters span the whole TASK // and are cleared only by an explicit new task (`set_task_tier` with // `new_task: true`) or an approved task-budget continuation — never by an // ordinary user message. let taskDispatchCount = 0; let taskResearchCount = 0; let taskLabel: string | null = null; // The task clock charges ACTIVE time only — turns that ran, minus the time the // dispatcher spent blocked on ask_user. Raw wall clock would bill the human's // lunch break, an overnight pause, and every long answer against the task, and // at small tier's 45-minute envelope that hard-stops a task with two dispatches // spent. A false stop is worse than no stop: it teaches people to reset the // task window reflexively, which is the one thing that must stay deliberate. // The explicit clock state makes `active` authoritative: a stale timestamp can // never turn inter-turn idle time into task work. let taskClock = createTaskClock(); // Review dispatches spent on this task (review-round cap). let taskReviewRounds = 0; // Task tier (complexity triage): declared by the dispatcher via set_task_tier. // TASK-scoped, not turn-scoped, and it moves by ratchet — down freely, up only // with a stated reason (applyTierChange). Null until declared; the first // dispatch assumes DEFAULT_TASK_TIER. Caps come from the tier envelope. let taskTier: string | null = null; // Was the tier ASSUMED by the hub rather than declared by the dispatcher? The // distinction matters to the ratchet: an assumed tier must not turn the // dispatcher's own first triage call into an "escalation" that needs a reason. let taskTierAssumed = false; // Correctness obligations are independent of the spend tier. They persist for // the current task and reset only through the explicit new-task tool path. let processState = createProcessState(); let taskTriage: ReturnType | null = null; let taskTriageTestTransportActive = false; let taskTriageActivity: TaskTriageActivity | null = null; let taskTriageConfigStatus: "active" | "off" | "invalid" = "off"; let taskTriageConfigured = false; let processPersistenceBlocked = false; let pendingTaskTransition = false; let transitionDiversions = 0; let transitionHintDelivered = false; let transitionBatchRefused = false; let orchestratorSelfReadUsed = 0; const processBlock = () => processPersistenceBlocked ? { reason: "process_state_corrupt", message: "Process persistence is blocked; dependent effects remain refused. Ask the human to run /af-task-triage-recover for a checked in-session append/readback; if it refuses, inspect or resume a valid session. Never edit session JSONL or reset obligations." } : pendingTaskTransition ? { reason: "task_transition_pending", message: taskTransitionRecoveryHint } : null; const persistProcessVerdict = (state: ProcessObligationState, verdict: ProcessVerdict) => { try { pi.appendEntry("agent-hub-process-state", processAuditRecord(state, verdict)); } catch { processPersistenceBlocked = true; } }; // Duplicate-dispatch guard: fingerprints of (agent, task) already dispatched // THIS turn. Auto-research resumes and /af-agents-restart call dispatchAgent // directly, so only real dispatcher tool calls are guarded. const turnDispatchFingerprints = new Set(); // ── External-blocker circuit breaker (external-blocker.js) ── // Set when a specialist reports it needs something outside the fleet's reach; // gates the next dispatch until the human has been addressed. Cleared by an // ask_user call, by a new user turn (the human spoke), and by a new task. let externalBlockers: { agent: string; what: string }[] = []; let externalBlockerAcknowledged = false; let externalBlockerRefusedOnce = false; // Per-run artifact archive retention (run-namespace.js). let runHistoryKeep: number | null = DEFAULT_RUN_HISTORY_KEEP; // ── Drift watchdog (drift-watchdog.js) ── // Hub-wide setting from the overrides file, live-switchable via /af-watchdog; // per-agent overrides ("on"/"off") win over it. In operator work mode a // dispatch_agent `watchdog` param wins over both. In orchestrator work mode the // watchdog auto-arms unless hub or per-agent is explicitly off; dispatch // `watchdog: false` cannot disarm it. let watchdogSetting: string = DEFAULT_WATCHDOG_SETTING; let watchdogJudgeModel: string | null = null; let watchdogSystem1: WatchdogSystem1Session | null = null; const communicationStore = createCommunicationStore(); let triageRuntime: ReturnType | null = null; let proactiveRuntime: ReturnType | null = null; let system1Snapshot: ReturnType = normalizeSystem1Config(null); let proactiveConfig: ProactiveConfig | null = null; let proactiveHubDeliveries = 0; const proactiveReportInput = () => proactiveRuntime ? { records: proactiveRuntime.records, history: proactiveRuntime.findings.history, current: proactiveRuntime.findings.current, activity: proactiveRuntime.activity.live(), feedback: { hubDelivered: proactiveHubDeliveries, nativeDelivered: null } } : undefined; let hubTaskText: string | undefined; const hubCapture = createHubCapture({ root: () => currentCtx?.cwd || process.cwd(), task: () => hubTaskText, policyRoots:()=>sessionOverrides?.policyRoots }); let watchdogActivity: WatchdogActivity | null = null; const watchdogAgentOverrides = new Map(); // ── Per-turn cost report (/af-hub-report) ── let turnReport: TurnReport = freshTurnReport(); let lastTurnReport: TurnReport | null = null; const sessionTotals = createSessionTotals(); // Session-wide delegated-spend counter (tokens across all delegate children), // surfaced in the status line. Resets on session_start. let delegatedTokens = 0; const noProgress = createNoProgressGuard((type, data) => pi.appendEntry(type, data)); const taskTriageMetadata = () => { const current = taskTriage?.current; return { taskId: noProgress.taskId(), inputRevision: taskTriage?.inputRevision, configuredStatus: taskTriageConfigStatus, runtimeBlocks: [...(processPersistenceBlocked ? ["process_persistence_blocked" as const] : []), ...(pendingTaskTransition ? ["task_transition_pending" as const] : [])], assessment: current ? { taskId: current.taskId, evaluationId: current.evaluationId, inputRevision: current.revision, assessment: current.assessment } : undefined }; }; let taskIdentityReset: ReturnType; let unknownToolCounter = createUnknownToolCounter({ limit: 3 }); const toolCatalogRuntime = createToolCatalogRuntime(catalogSnapshot("operator", [])); let latestToolCatalogDelta: ToolCatalogDelta | null = null; let latestUnknownToolNotice = ""; const seenUnknownToolCalls = new Set(); const resetUnknownToolCounterForCurrentTask = () => { unknownToolCounter.resetForNewTask(noProgress.taskId()); seenUnknownToolCalls.clear(); latestUnknownToolNotice = ""; }; let readWorkMode: () => string = () => "operator"; const budgetCtx = createBudgetContext({ getBudgetOverrides: () => budgetOverrides, getWorkMode: () => readWorkMode(), getTurnDispatchCount: () => turnDispatchCount, setTurnDispatchCount: value => { turnDispatchCount = value; }, getTurnResearchCount: () => turnResearchCount, setTurnResearchCount: value => { turnResearchCount = value; }, getTurnBudgetAskUserWaitMs: () => turnBudgetAskUserWaitMs, setTurnBudgetAskUserWaitMs: value => { turnBudgetAskUserWaitMs = value; }, resetBudgetRecovery: () => budgetRecovery.reset(), resetNoProgress: (persistTaskIdentity) => noProgress.reset(persistTaskIdentity), resetUnknownToolCounter: resetUnknownToolCounterForCurrentTask, getTaskContinuationCount: () => taskContinuationCount, setTaskContinuationCount: value => { taskContinuationCount = value; }, getTurnContinuationCount: () => turnContinuationCount, setTurnContinuationCount: value => { turnContinuationCount = value; }, getTaskDispatchCount: () => taskDispatchCount, setTaskDispatchCount: value => { taskDispatchCount = value; }, getTaskResearchCount: () => taskResearchCount, setTaskResearchCount: value => { taskResearchCount = value; }, getTaskLabel: () => taskLabel, setTaskLabel: value => { taskLabel = value; }, getTaskClock: () => taskClock, setTaskClock: value => { taskClock = value; }, getTaskReviewRounds: () => taskReviewRounds, setTaskReviewRounds: value => { taskReviewRounds = value; }, getTaskTier: () => taskTier, setTaskTier: value => { taskTier = value; }, getTaskTierAssumed: () => taskTierAssumed, setTaskTierAssumed: value => { taskTierAssumed = value; }, clearTurnDispatchFingerprints: () => turnDispatchFingerprints.clear(), clearTaskCapabilities: () => workModePolicy.resetCapabilities(), clearExternalBlockers: () => { externalBlockers = []; externalBlockerAcknowledged = false; externalBlockerRefusedOnce = false; }, resolveIncomingCapabilities: () => { resolveIncomingCapabilities("", true); }, applyWorkModeTools: () => { applyWorkModeTools(); }, getTurnReport: () => turnReport, setStatus: (key, value) => { widgetCtx?.ui?.setStatus(key, value); }, getAuditContext: () => ({ cwd: identity?.cwd ?? currentCtx?.cwd, sessionId: identity?.session_id, project: identity?.project }), appendEntry: (type, data) => { pi.appendEntry(type, data); }, executionHistory, }); const { currentBudget, currentTaskBudget, taskCounters, taskActiveElapsedMs, turnBudgetActiveElapsedMs, renewTurnBudgetWindow, continueTaskBudgetWindow, closeTurnActiveTime, resetTaskWindow, hubAuditIdentity, hubLocationSuffix, taskResetSnapshot, budgetContinuationSnapshot, appendTaskResetEntry, appendBudgetContinuationEntry, updateModeStatus, ensureTaskTier, } = budgetCtx; const budgetRecovery = createBudgetRecovery({ check: operation => { const task = checkTaskBudget(operation, taskCounters(), currentTaskBudget(), taskActiveElapsedMs(), taskTier); if (task) return { ...task, kind: "task" }; const turn = checkTurnBudget(operation, { dispatches: turnDispatchCount, research: turnResearchCount }, currentBudget(), turnBudgetActiveElapsedMs(), taskTier); return turn ? { ...turn, kind: "turn" } : null; }, language: () => userLanguage, ask: (id, params, ctx, signal) => requestRuntimeAsk(pi.events, id, params, ctx, signal), startWait: id => executionHistory.startAskUser(id), endWait: (id, sameTask) => { const wait = executionHistory.endAskUser(id, Date.now()); if (!sameTask) return; if (wait > 0) taskClock = addTaskClockWait(taskClock, wait); turnBudgetAskUserWaitMs += wait; }, renew: (refusal, correlation, ctx) => { const at = Date.now(), prior = budgetContinuationSnapshot(refusal.kind, at); if (refusal.kind === "task") continueTaskBudgetWindow(at); else renewTurnBudgetWindow(at); appendBudgetContinuationEntry(refusal.kind, refusal.reason, prior, ctx, correlation); }, }); taskIdentityReset = createReservedTaskIdentityReset(budgetRecovery, noProgress); // ── Verification Contract: assertion ledger (advisory) ── // Mutable ownership remains here; the extracted runtime receives explicit ports. let assertions: Assertion[] = []; const assertionsArtifactsCtx = createAssertionsArtifactsContext({ getAssertions: () => assertions, getSessionDir: () => sessionDir, getRunHistoryKeep: () => runHistoryKeep, setStatus: (key, value) => { widgetCtx?.ui?.setStatus(key, value); }, }); const { persistAssertions, assertionStatusLine, renderAssertionLedgerLines, renderAssertionLedgerText, updateAssertionStatus, artifactsRoot, ensureArtifactsLayout, archivePreviousRun, loadInputArtifacts, appendInputArtifacts, writeRunArtifact, evidencePathExists, listArtifactFiles, renderArtifactIndexText, appendMachineHandoffSections, } = assertionsArtifactsCtx; const modelPolicy = createModelPolicy({ getAllDefs: () => allAgentDefs, getActiveDef: name => agentStates.get(name)?.def, getResearchDefs: () => researchPersonas, refreshUi: () => updateWidget(), }); const { allowedModels, substitutedModel, resolvedModel, resolvedSubagentModel, resolvedThinking, switchablePersonaDef, allKnownModels, } = modelPolicy; let profileCommandContext: any; const profileActivation = createProfileActivation({ busy: () => !profileCommandContext?.isIdle?.() || profileWorkInFlight()>0 || Array.from(agentStates.values()).some(state=>state.status==='running') || Array.from(researchStates.values()).some(state=>state.status==='running'), defs: () => allAgentDefs, available: async () => { await profileCommandContext.modelRegistry.refresh?.(); return (profileCommandContext.modelRegistry.getAvailable?.()??[]).map((m:any)=>`${m.provider}/${m.id}`); }, dispatcher: () => profileCommandContext?.model ? {model:`${profileCommandContext.model.provider}/${profileCommandContext.model.id}`,thinking:pi.getThinkingLevel()} : undefined, setDispatcher: async selection => { const slash=selection.model.indexOf('/'); const model=profileCommandContext.modelRegistry.find(selection.model.slice(0,slash),selection.model.slice(slash+1)); if(!model || !await pi.setModel(model)) return false; if(selection.thinking) pi.setThinkingLevel(selection.thinking as any); return true; }, apply: profile => modelPolicy.applyProfile(profile), }); pi.on('model_select', async (event,ctx) => { contextWindow=event.model.contextWindow||0; if(profileActivation.switching()) return; const active=readActiveProfile();if(!active || !hasExplicitDispatcher(active.profile)) return; try { assertProfileModel(`${event.model.provider}/${event.model.id}`,active); } catch(error) { ctx.abort();ctx.ui.notify(String(error),'error'); if(hasExplicitDispatcher(active.profile)) { const target=dispatcherSelection(active.profile).model, slash=target.indexOf('/'); const model=ctx.modelRegistry.find(target.slice(0,slash),target.slice(slash+1)); if(model) await pi.setModel(model); } } }); pi.on('before_provider_request', async (_event,ctx) => { const active=readActiveProfile(); if(!ctx.model || !active || !hasExplicitDispatcher(active.profile)) return; try { assertProfileModel(`${ctx.model.provider}/${ctx.model.id}`,active); } catch(error) {ctx.abort();ctx.ui.notify(String(error),'error');} }); function appendDeclaredScope(task: string, scopeGlobs: string[]): string { if (!scopeGlobs || scopeGlobs.length === 0) return task; return task + `\n\n## Declared scope — advisory guardrail\nStay within these paths/globs when changing files; changes outside them will be flagged to the dispatcher for a human decision, not auto-reverted.\n${scopeGlobs.map(s => `- ${s}`).join("\n")}`; } function loadAgents(cwd: string): void { if (sessionDir) closeEvidenceSession(sessionDir); loadAgentConfiguration(cwd, { setSessionDir: hubStateCtx.setSessionDir, getSessionDir: hubStateCtx.getSessionDir, archivePreviousRun, ensureArtifactsLayout, resetAssertions: () => { assertions = []; }, setAgentDefs: value => { allAgentDefs = value; }, setTeams: value => { teams = value; }, setModelProfiles: value => { modelProfiles = value; }, setModelProfileErrors: errors => { modelProfileErrors=errors; }, setDispatchPolicy: value => { dispatchPolicy = value; }, setDispatchPolicyWarnings: value => { dispatchPolicyWarnings = value; }, }); pruneEvidenceSessions(sessionDir, runHistoryKeep); } const agentStateFactory = createAgentStateFactory(() => sessionDir); const { sessionHealthIo, adoptableSessionFile, freshAgentState } = agentStateFactory; // Auto-size grid columns based on team size function recomputeGrid() { gridCols = gridColumnsForSize(agentStates.size); } const rosterPolicy = createRosterPolicy({ getTeams: () => teams, getAllDefs: () => allAgentDefs, getStates: () => agentStates, getActiveTeamName: () => activeTeamName, setActiveTeamName: value => { activeTeamName = value; }, clearBackendNotices: () => comsMissNotified.clear(), createFreshState: freshAgentState, adoptSession: adoptableSessionFile, quarantineSession: agentStateFactory.quarantine, persist: team => pi.appendEntry(NATIVE_ROSTER_ENTRY_TYPE, persistedNativeRosterState(team)), recompute: recomputeGrid, refreshUi: () => updateWidget(), displayName, orchestratorNeedsRosterAfterDrop: size => orchestratorNeedsRoster(getWorkMode(), size), }); const { activateTeam, persistActiveRoster, add: rosterAdd, drop: rosterDrop } = rosterPolicy; // ── Shared model presentation ───────────────── // These formatters serve the extracted grid and the remaining composition-root // call sites. Keep one implementation so cards, fleet rows, menus, fallback // notices, and the footer retain the same presentation semantics. function shortModel(model: string | undefined): string { return model ? model.split("/").pop()! : "default"; } // A " (code)" thinking badge for display, or "" when the level is off. function thinkingSuffix(rawThinking: string | undefined): string { const code = abbrevThinking(resolveThinkingLevel(rawThinking)); return code ? ` (${code})` : ""; } // The model + thinking badge a persona would dispatch with: "gpt-5.5 (xh)". function modelWithThinking(def: AgentDef): string { return shortModel(resolvedModel(def)) + thinkingSuffix(resolvedThinking(def)); } // ── Shared fleet source + below-editor widget ─────────────────────────── const fleetSource = createFleetSource({ getAgents: () => agentStates, getResearch: () => researchStates, getPeerInputs: format => fleetPeerInputs(format), getPeerCards: () => peerCards, getPendingReplies: () => pendingReplies.values(), displayName, modelForAgent: state => state.lastBackend === "coms" ? shortModel(state.comsPeerModel) : modelWithThinking(state.def), modelForResearch: state => shortModel(state.model) + thinkingSuffix(resolvedThinking(state.def)), modelForPeer: abbreviateModel, getSystem1: () => watchdogActivity?.live() ?? null, getTaskTriage: () => { if (!taskTriageConfigured && !processState.additions?.length) return null; try { return { ...taskTriageMetadata(), process: { schema: processState.schema, ...evaluateProcessObligations(processState, { writable: true, budgetTier: taskTier ?? DEFAULT_TASK_TIER }) } }; } catch { return { ...taskTriageMetadata(), process: { schema: "invalid" } }; } }, getProactive: () => proactiveRuntime ? { records: proactiveRuntime.records, history: proactiveRuntime.findings.history, current: proactiveRuntime.findings.current, activity: proactiveRuntime.activity.live() } : null, }); let fleetActions: ReturnType> | null = null; let fleetUiGeneration = 0; const gridUI = createGridUI({ getWidgetContext: () => widgetCtx, getRows: now => fleetSource.rows(now, { showFinished: true }), getSnapshot: now => { const source = fleetSource.snapshot(now); return { rows: buildFleetRows(source, { showFinished: true }), proactive: source.proactive }; }, handleIntent: async intent => { if (!fleetActions || !widgetCtx) return; if (intent.type === "system1") { await gridUI.withSuspended(() => openSystem1Communication(widgetCtx!, communicationStore)); return; } if (intent.type === "open") await gridUI.withSuspended(() => fleetActions!.open(intent.key, intent.runToken, widgetCtx!)); else await fleetActions.execute(intent.type, intent.key, intent.runToken, widgetCtx); }, }); const { updateWidget } = gridUI; refreshFleetUi = updateWidget; // ── Delegation observability ───────────────── const dispatchObservability = createDispatchObservability({ getSessionDir: () => sessionDir, getDelegatedTokens: () => delegatedTokens, setDelegatedTokens: value => { delegatedTokens = value; }, getWidgetContext: () => widgetCtx, executionHistory, displayName, safeAgentKey, safePathWithin, createTranscriptStore: createFleetTranscriptStore, appendTimelineText, appendTimelineEvent, updateWidget, }); const { startDelegationWatch } = dispatchObservability; // ── Dispatch Agent (returns Promise) ───────── // ── Extracted coms dispatch, drift judge, and return extraction ── const dispatchComs = createDispatchComs({ getIdentity: () => identity, resolveTarget: target => coms.resolveTarget(target), send: (params, auditExtra) => coms.send(params, auditExtra), getPendingReply: msgId => pendingReplies.get(msgId), deletePendingReply: msgId => { pendingReplies.delete(msgId); }, getSessionDir: () => sessionDir, getWatchdogJudgeModel: () => watchdogJudgeModel, getResearcherModel: () => { const researcherDef = allAgentDefs.find(def => def.name.toLowerCase() === "researcher"); return researcherDef ? resolvedModel(researcherDef) ?? null : null; }, displayName, safeAgentKey, safePathWithin, appendInputArtifacts, appendDeclaredScope, buildRulesProtocol, buildDocsProtocol, updateWidget, spawnPiAgent, }); const { dispatchViaComs, runDriftJudge, runReturnExtraction } = dispatchComs; const nativeDispatch = createDispatchNative({ prepareDiscovery:input=>prepareDiscoveryNative(pi,input), launchIdentity:()=>noProgress.taskToken(), nativeAdmission:persona=>!processBlock()&&!provisionalCapabilityRefusal('fleet')&&!processPreEffectGate(processState,'child',persona)&&!checkTaskBudget('dispatch',{...taskCounters(),dispatches:Math.max(0,taskDispatchCount-1)},currentTaskBudget(),taskActiveElapsedMs(),taskTier)&&!checkTurnBudget('dispatch',{dispatches:Math.max(0,turnDispatchCount-1),research:turnResearchCount},currentBudget(),turnBudgetActiveElapsedMs(),taskTier), getAgentState: key => agentStates.get(key), getProcessState: () => processState, listAgentStates: () => Array.from(agentStates.values()), getSessionDir: () => sessionDir, getDispatchPolicy: () => dispatchPolicy, isComsReady: () => comsReady, getIdentity: () => identity, peersInScope: () => peersInScope(), wasComsMissNotified: personaKey => comsMissNotified.has(personaKey), markComsMissNotified: personaKey => { comsMissNotified.add(personaKey); }, startMonitorChild: (input, env) => { const monitorStart = monitorTurnId; return monitorStart ? monitorBridge?.startChild({ ...input, parentId: monitorStart }, env) : undefined; }, finalizeMonitorChild: (task, output, status) => monitorBridge?.finalizeChildFor(task, output, status), registerMonitorWaitOnly: (monitorKey, state) => monitorBridge?.registerWaitOnly(monitorKey, () => state.comsAbort?.()), registerMonitorProcess: (task, proc) => monitorBridge?.registerOwnedProcessFor(task, proc), appendMonitorOutput: (task, delta) => monitorBridge?.appendOutputFor(task, delta), getContextWindow: () => contextWindow, currentBudget, bumpRecycle: () => { turnReport.recycles++; sessionTotals.recycles++; }, bumpDriftStop: () => { turnReport.driftStops++; sessionTotals.driftStops++; }, getSessionHealthIo: () => sessionHealthIo, getSafetyHarnessPath: () => safetyHarnessPath, getDelegateExtensionPath: () => delegateExtPath, getReconSearchTimeoutMs: () => reconSearchTimeoutMs, getProjectDocsPaths: () => projectDocsPaths, getUserLanguage: () => userLanguage, getWatchdogSetting: () => watchdogSetting, getWatchdogAgentOverride: key => watchdogAgentOverrides.get(key), getWatchdogSystem1: () => watchdogSystem1, getWatchdogActivity: () => watchdogActivity, getProactiveRuntime: () => proactiveRuntime, getProactiveConfig: () => proactiveConfig, getProactiveCapture: () => proactiveRuntime ? hubCapture : null, getPolicyRoots: () => sessionOverrides?.policyRoots, getWorkMode: () => getWorkMode(), providerSemaphore, executionHistory, displayName, shortModel: model => shortModel(model), resolvedModel, resolvedThinking, resolveThinkingLevel, resolvedSubagentModel, substitutedModel, modelWindowLookup, specialistProjectPolicyPaths, guardrailEnv, appendInputArtifacts, appendDeclaredScope, flushTimelineStore, appendTimelineText, appendTimelineEvent, createTranscriptStore: createFleetTranscriptStore, updateWidget, startDelegationWatch, dispatchViaComs, runDriftJudge, notifyProviderQueue, spawnPiAgentWithModelFallback, }); const { dispatchAgent } = nativeDispatch; // Shared child guardrail environment. Dispatch-native and research use the // same root-owned exemptions and coms escalation endpoint. function guardrailEnv(agentId: string): Record { const env: Record = { [AGENT_ID_ENV]: agentId }; if (sessionOverrides?.policyRoots) env[FILESYSTEM_POLICY_ROOTS_ENV]=JSON.stringify(sessionOverrides.policyRoots); if (exemptionsFile) env[EXEMPTIONS_FILE_ENV] = exemptionsFile; if (comsReady && identity) env[ASK_ENDPOINT_ENV] = identity.endpoint; return env; } // ── Research helper runtime ──────────────────── // The composition root owns the mutable bindings; the runtime reaches them only // through these explicit ports so later controls can keep stable handles. const researchRuntime = createResearchRuntime({ prepareDiscovery:input=>prepareDiscoveryNative(pi,input), launchIdentity:()=>noProgress.taskToken(), researchAdmission:()=>!processBlock(), getResearchStates: () => researchStates, setResearchStates: value => { researchStates = value; }, getNextResearchId: () => nextResearchId, setNextResearchId: value => { nextResearchId = value; }, hubState: hubStateCtx, budget: budgetCtx, artifacts: assertionsArtifactsCtx, executionHistory, providerSemaphore, getSafetyHarnessPath: () => safetyHarnessPath, getReconSearchTimeoutMs: () => reconSearchTimeoutMs, getContextWindow: () => contextWindow, resolvedModel, resolvedThinking, resolveThinkingLevel, fallbackModelFor, substitutedModel, modelWindowLookup, guardrailEnv, notifyProviderQueue, spawnPiAgentWithModelFallback, getProjectPolicyPaths: specialistProjectPolicyPaths, getProjectDocsPaths: () => projectDocsPaths, nativeResearchSystemPrompt, requireSafetyHarness, shortModel, displayName, flushTimelineStore, appendTimelineText, appendTimelineEvent, createTranscriptStore: createFleetTranscriptStore, onElapsed: () => updateWidget(), }); // ── Embedded coms: shared registry, transport, and pool core ── const peersInScope = () => coms.peersInScope(); const resolveTarget = (target: string) => coms.resolveTarget(target); const poolPresentation = createPoolPresentation({ getIdentity: () => identity, getDisplayProject: () => coms.scope.displayProject, includeExplicitPeers: () => coms.scope.includeExplicit, getPeerCards: () => peerCards, readProjectEntries: readAllRegistryEntries, readAllEntries: readAllRegistryEntriesAcrossProjects, truncate: truncateToWidth, }); const fleetPeerInputs = poolPresentation.peerInputs; // ── Extracted tool execution wiring ── // Mutable fleet state remains composition-owned; executor modules receive ports. let herdrFleetReady = false; const hubSpawnedPeers = new Map(); const markPeerAddressed = (name: string) => { const entry = hubSpawnedPeers.get(String(name || "").toLowerCase()); if (entry) entry.addressed = true; }; let lastHubPiSpawnAt: number | null = null; let askUserAvailable = false; let baselineTools: string[] = []; let contextPressureState: ContextPressureState = createContextPressureState(); const pressureRootState = createContextPressureRootState(); function pendingCapabilityOperations(): PendingOperation[] { const pending: PendingOperation[] = []; if (pendingTaskTransition) pending.push({ pack: "fleet", kind: "task-transition" }); if (processOpenObligations(processState).some(stage => stage === "plan" || stage === "review")) pending.push({ pack: "fleet", kind: "process-stage" }); if (Array.from(agentStates.values()).some(state => state.status === "running") || Array.from(researchStates.values()).some(state => state.status === "running")) pending.push({ pack: "fleet", kind: "child" }); if (pendingReplies.size > 0 || pendingHandoff) pending.push({ pack: "peer", kind: "message" }); if (hubSpawnedPeers.size > 0) pending.push({ pack: "workspace", kind: "pane" }); return pending; } function recordToolCatalogChange(change: { fromMode: WorkMode; toMode: WorkMode; previous: readonly string[]; next: readonly string[]; reason: "refresh" | "mode_switch" }): void { const { snapshot, delta } = toolCatalogRuntime.reconcile(change); latestToolCatalogDelta = delta; unknownToolCounter.noteCatalogChange(delta.catalogVersion); const evidenceRef = `session-entry:${TOOL_CATALOG_ENTRY_TYPE}:${delta.catalogVersion}`; let persisted = false; try { pi.appendEntry(TOOL_CATALOG_ENTRY_TYPE, { snapshot, delta, reason: change.reason }); persisted = true; } catch {} if (persisted && delta.evidence.changed) noProgress.establishToolStateChange(delta.evidence.previousCatalogVersion, delta.evidence.catalogVersion, evidenceRef); } function capabilityContextState(): ContextState { if (contextPressureState.phase === "warning") return "approaching-compaction"; if (contextPressureState.phase !== "normal" || contextPressureState.pressure === "imminent") return "imminent-compaction"; if (contextPressureState.pressure === "approaching") return "approaching-compaction"; const percent = currentCtx?.getContextUsage?.()?.percent; if (typeof percent === "number" && percent >= 90) return "imminent-compaction"; if (typeof percent === "number" && percent >= 80) return "approaching-compaction"; return "normal"; } const workModePolicy = createWorkModePolicy({ getBaselineTools: () => baselineTools, getRosterSize: () => agentStates.size, getDeterministicToolsEnabled: () => resolveAssist(readActiveProfile()?.profile.assist)['deterministic-tools'], getAgenticAskEnabled: () => agenticHubEnabled(pi), getFileDiscoveryEnabled: () => discoveryHubEnabled(pi), getDispatchTriageEnabled: () => triageRuntime?.available === true, activateFallbackRoster: ctx => { if (!rosterPolicy.activateFirstValidTeam()) return; workModePolicy.clearRosterRecovery(); persistActiveRoster(); setTimeout(replayDeferredRecoveryInputs, 0); updateWidget(); ctx.ui.setStatus("agent-team", `Team: ${activeTeamName} (${agentStates.size})`); }, getActiveTeamName: () => activeTeamName, getComsReady: () => comsReady, getHerdrReady: () => herdrFleetReady, getAskUserAvailable: () => askUserAvailable, getIdentityLabel: () => identity ? `${identity.name}@${identity.project}` : null, getTaskTier: () => taskTier, getPendingOperations: pendingCapabilityOperations, getContextState: capabilityContextState, getActiveTools: () => pi.getActiveTools(), setActiveTools: tools => pi.setActiveTools(tools), recordToolCatalog: recordToolCatalogChange, persist: (type, data) => pi.appendEntry(type, data), replayDeferredInputs: replayDeferredRecoveryInputs, watchdogArmed: mode => resolveWatchdogActive(undefined, undefined, watchdogSetting, mode), }); const { getWorkMode, getCapabilityResolution, resolveIncomingCapabilities, provisionalCapabilityRefusal, applyWorkModeTools, modelWorkBlockedByRosterRecovery, statusText: workModeStatusText, applySelection: applyWorkModeSelection, openPicker: openWorkModePicker, } = workModePolicy; readWorkMode = () => getWorkMode(); const toolCtx: ToolContext = createToolExecutionOrchestration({ dispatch: { state: { getTurnDispatchCount: () => turnDispatchCount, setTurnDispatchCount: value => { turnDispatchCount = value; }, getTurnResearchCount: () => turnResearchCount, setTurnResearchCount: value => { turnResearchCount = value; }, getTaskDispatchCount: () => taskDispatchCount, setTaskDispatchCount: value => { taskDispatchCount = value; }, getTaskResearchCount: () => taskResearchCount, setTaskResearchCount: value => { taskResearchCount = value; }, getTaskReviewRounds: () => taskReviewRounds, setTaskReviewRounds: value => { taskReviewRounds = value; }, getTaskTier: () => taskTier, getTurnReport: () => turnReport, getSessionTotals: () => sessionTotals, getProcessState: () => processState, setProcessState: value => { processState = value; }, persistProcessVerdict, processBlock, getTurnDispatchFingerprints: () => turnDispatchFingerprints, getExternalBlockers: () => externalBlockers, getExternalBlockerAcknowledged: () => externalBlockerAcknowledged, setExternalBlockerAcknowledged: value => { externalBlockerAcknowledged = value; }, getExternalBlockerRefusedOnce: () => externalBlockerRefusedOnce, setExternalBlockerRefusedOnce: value => { externalBlockerRefusedOnce = value; }, isAskUserAvailable: () => askUserAvailable, getUserLanguage: () => userLanguage, getSessionDir: () => sessionDir, getAgentStates: () => agentStates as any, getAssertions: () => assertions, getResearchPersonas: () => researchPersonas, getActiveWritableDispatches: () => activeWritableDispatches, setActiveWritableDispatches: value => { activeWritableDispatches = value; }, getWritableOverlapCounter: () => writableOverlapCounter, setWritableOverlapCounter: value => { writableOverlapCounter = value; }, }, budget: budgetCtx, budgetRecovery, noProgress, artifacts: assertionsArtifactsCtx, research: researchRuntime, provisionalCapabilityRefusal, dispatchAgent, runReturnExtraction, extractNeedsResearch, extractAskUserQuestions, contextPressure: percent => percent >= CONTEXT_WARN_THRESHOLD, displayName, resolvedAgentModel: (def, ctx) => resolvedModel(def as AgentDef) ?? (ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : undefined), getToolCatalogVersion: () => toolCatalogRuntime.catalogForMessage().catalogVersion, }, actions: { budget: budgetCtx, artifacts: assertionsArtifactsCtx, hubState: hubStateCtx, provisionalCapabilityRefusal, getTaskTier: () => taskTier, setTaskTier: value => { taskTier = value; }, getTaskTierAssumed: () => taskTierAssumed, setTaskTierAssumed: value => { taskTierAssumed = value; }, getProcessState: () => processState, setProcessState: value => { processState = value; }, processBlock, adoptTaskTriage: async (id, newTask, expandedScope) => { if (processPersistenceBlocked) throw new Error("corrupt process state"); const next = await adoptTaskTriageState({ runtime: taskTriage, taskId: id, newTask, expandedScope, pendingTransition: pendingTaskTransition, state: processState, persist: state => pi.appendEntry("agent-hub-process-state", processAuditRecord(state, evaluateProcessObligations(state, { writable: true, budgetTier: taskTier ?? DEFAULT_TASK_TIER }))), onPostCommitFailure: () => { processPersistenceBlocked = true; }, }); pendingTaskTransition = false; transitionDiversions = 0; transitionHintDelivered = false; transitionBatchRefused = false; try { pi.appendEntry(TASK_TRANSITION_ENTRY_TYPE, { pending: false }); } catch { processPersistenceBlocked = true; } return next; }, persistProcessState: value => persistProcessVerdict(value, evaluateProcessObligations(value, { writable: true, budgetTier: taskTier ?? DEFAULT_TASK_TIER })), getTaskDispatchCount: () => taskDispatchCount, getTaskResearchCount: () => taskResearchCount, getTurnReport: () => turnReport, getAssertions: () => assertions, setAssertions: value => { assertions = value; }, currentTaskId: () => noProgress.taskId(), currentRevision: ctx => worktreeRevision(ctx.cwd || process.cwd(), []), adoptReservedTaskId: (id, resetTaskWindow) => taskIdentityReset.run(id, resetTaskWindow), confirmTaskSupersession: (input, ctx, signal) => confirmTaskSupersession(input, ctx, { language: () => userLanguage, currentTaskId: () => noProgress.taskId(), ask: (id, params, askCtx, askSignal) => requestRuntimeAsk(pi.events, id, params, askCtx, askSignal), startWait: id => executionHistory.startAskUser(id), endWait: (id, sameTask) => { const wait = executionHistory.endAskUser(id, Date.now()); if (sameTask && wait > 0) { taskClock = addTaskClockWait(taskClock, wait); turnBudgetAskUserWaitMs += wait; } } }, signal), getAgentStates: () => agentStates, rosterAdd, rosterDrop, getIdentity: () => identity, getComs: () => coms, resolveTarget, appendMachineHandoffSections, markPeerAddressed, }, herdr: { getProcessState: () => processState, processBlock, provisionalCapabilityRefusal, isFleetReady: () => herdrFleetReady, isComsReady: () => comsReady, getIdentity: () => identity, getCurrentContext: () => currentCtx, peersInScope, getComsPeerNames: () => peersInScope().map(peer => peer.name), herdr: herdrApi, readEnvFile: file => fs.readFileSync(file, "utf-8"), envFileExists: file => fs.existsSync(file), getLastPiSpawnAt: () => lastHubPiSpawnAt, setLastPiSpawnAt: value => { lastHubPiSpawnAt = value; }, recordSpawnedPeer: (name, paneId) => { hubSpawnedPeers.set(name.toLowerCase(), { name, paneId, addressed: false }); }, }, }); function triageInput(task: string, scope: string[], language: string, domain: string): TriageInput { const taskGate = checkTaskBudget("dispatch", budgetCtx.taskCounters(), budgetCtx.currentTaskBudget(), budgetCtx.taskActiveElapsedMs(), taskTier); const turnGate = checkTurnBudget("dispatch", { dispatches: turnDispatchCount, research: turnResearchCount }, budgetCtx.currentBudget(), budgetCtx.turnBudgetActiveElapsedMs(), taskTier); const candidates = [...agentStates.values()].map(s => { const name = s.def.name.toLowerCase(); const research = ["researcher", "deep-researcher", ...researchPersonas.map(p => p.name.toLowerCase())].includes(name); const gate = processBlock() ?? processPreEffectGate(processState, "child", name) ?? (processAllowsPersona(processState,name) ? null : checkTierPersonaGate(taskTier,name)) ?? checkReviewRoundCap(taskTier,name,taskReviewRounds) ?? checkDocsLane(name, scope, undefined) ?? taskGate ?? turnGate; return { name, description: s.def.description, ...(research ? { excluded: "research_persona" } : s.status === "running" ? { excluded: "busy" } : gate ? { excluded: gate.reason } : {}) }; }); return { taskId: noProgress.taskId(), task, scope, language, domain, candidates, constraints: JSON.stringify({ tier: taskTier, process: { risk: processState.risk, scope: processState.scope }, externalBlocked: externalBlockers.length > 0 && !externalBlockerAcknowledged }), complete: !(externalBlockers.length > 0 && !externalBlockerAcknowledged) }; } registerAgenticHub(pi); registerDiscoveryHub(pi); registerDispatchTriage(pi, { runtime: () => triageRuntime, input: triageInput, blocked: () => provisionalCapabilityRefusal("fleet") }); // Keep the extracted tool surface flat and greppable in this composition root. registerDispatchAgent(pi, toolCtx, { disposition: (id, persona, reason) => triageRuntime?.disposition(id, persona, reason) ?? false, submitted: (id, persona, status) => triageRuntime?.submitted(id, persona, status) }); registerSpawnResearch(pi, toolCtx); registerRunFlow(pi, { sessionDir: () => sessionDir, taskId: () => noProgress.taskId(), processObligations: () => evaluateProcessObligations(processState, { writable: false, budgetTier: taskTier ?? DEFAULT_TASK_TIER }), effectiveScoutConfig: cwd => { const persona = resolveFlowPersona("researcher", cwd); const model = persona.model!; assertFlowProfileModel(model); return { model, profile: readActiveProfile()?.name ?? null, tools: persona.tools.split(",").map(value => value.trim()).filter(Boolean), fallback: persona.fallbackModel ?? null, allowlisted: true }; }, reserveBudget: async (params, ctx, signal) => { ensureTaskTier(); const blocked = await budgetRecovery.ensure("research", `flow scout: ${params.request}`, ctx, signal); if (blocked) return { charged: false, operation: "research", owner: "hub", refusal: blocked.message }; if (signal?.aborted) return { charged: false, operation: "research", owner: "hub", refusal: "Flow cancelled before budget charge." }; const taskBlock = checkTaskBudget("research", budgetCtx.taskCounters(), budgetCtx.currentTaskBudget(), budgetCtx.taskActiveElapsedMs(), taskTier); if (taskBlock) return { charged: false, operation: "research", owner: "hub", refusal: taskBlock.message }; const turnBlock = checkTurnBudget("research", { dispatches: turnDispatchCount, research: turnResearchCount }, budgetCtx.currentBudget(), budgetCtx.turnBudgetActiveElapsedMs(), taskTier); if (turnBlock) return { charged: false, operation: "research", owner: "hub", refusal: turnBlock.message }; turnResearchCount++; taskResearchCount++; turnReport.research++; sessionTotals.research++; budgetCtx.updateModeStatus(); return { charged: true, operation: "research", owner: "hub" }; }, }); registerSetTaskTier(pi, toolCtx); registerTeamAdjust(pi, toolCtx); registerVerificationContract(pi, toolCtx); registerComsTools(pi, toolCtx); registerFleetTools(pi, toolCtx); registerFilesystemTool(pi, { enabled: () => resolveAssist(readActiveProfile()?.profile.assist)['deterministic-tools'], readOnly: () => getWorkMode() === "orchestrator", sessionDir: () => sessionDir, policyRoots: () => sessionOverrides?.policyRoots, managedReadbackAllowed: handle => discoveryManagedReadbackAllowed(pi,handle), remainingSelfReadBytes: () => Math.max(0, 64 * 1024 - orchestratorSelfReadUsed), noteSelfReadBytes: bytes => { orchestratorSelfReadUsed += bytes; }, }); const researchControls = createResearchControls({ runtime: researchRuntime, refresh: () => {}, getAgents: () => agentStates, displayName, modelWorkBlocked: modelWorkBlockedByRosterRecovery, cancelWait: (state, kind) => cancelLocalWaitOnly({ abort: state.comsAbort, monitorBridge, monitorKey: monitorKeyForAgent(state.def.name, state.dispatchId ?? state.runCount), event: { kind } }), cancelOwned: state => { fenceOperatorCancel(state); cancelLocalOwnedProcess({ process: state.proc, monitorBridge, monitorKey: monitorKeyForAgent(state.def.name, state.dispatchId ?? state.runCount), treeKill: killPiTree }); }, restartSpecialist: async (state: AgentState, ctx) => { if (state.status === "running" && (state.proc || state.comsAbort)) { let resolveTermination!: () => void; const terminated = new Promise(resolve => { resolveTermination = resolve; }); state.onTerminate = resolveTermination; fenceOperatorCancel(state); if (state.proc) { state.killedByOperator = true; state.restarting = true; if (state.dispatchId) proactiveRuntime?.abort(safeAgentKey(state.def.name), state.dispatchId); killPiTree(state.proc); } else await cancelLocalWaitOnly({ abort: state.comsAbort, monitorBridge, monitorKey: monitorKeyForAgent(state.def.name, state.dispatchId ?? state.runCount), event: { kind: "restart" } }); await terminated; } state.sessionFile = null; const result = await dispatchAgent(state.def.name, state.task, ctx); pi.sendMessage({ customType: "agent-restart-result", content: `[${displayName(state.def.name)}] restarted by operator and ${result.exitCode === 0 ? "completed" : "failed"} in ${Math.round(result.elapsed / 1000)}s.`, display: true }, { deliverAs: "followUp", triggerTurn: true }); }, }); // ── ask_user Tool (dispatcher → human) ── // // We do NOT register `ask_user` here. The recommended companion package // `pi-ask-user` (see docs/pi-setup.md) owns that tool name with a richer // implementation. Registering our own conflicts regardless of load order: // - if we register first, pi-ask-user fails to load // - if pi-ask-user registers first, our registration fails // and pi has no synchronous probe at load time — `pi.getAllTools()` is // a runtime action method that throws when called from the factory. // // Instead, in `session_start` (where action methods ARE allowed) we check // `pi.getAllTools()`, gate `ask_user` into `setActiveTools` only if present, // and warn the user to `pi install npm:pi-ask-user` if it's missing. // ── Commands ───────────────────────────────── const commandCtx: CommandContext = { setWidgetContext: ctx => { widgetCtx = ctx; }, applyWorkModeSelection, getWorkModeStatusText: workModeStatusText, openWorkModePicker, handleBudgetContinue: async ctx => { await budgetRecovery.resume(ctx); }, handleTaskTriageRecover: () => { if (!processPersistenceBlocked) return false; try { const recovered = recoverTaskTriageProcessState({ state: processState, current: taskTriage?.current ? { taskId: taskTriage.current.taskId, evaluationId: taskTriage.current.evaluationId, inputRevision: taskTriage.current.revision, assessment: taskTriage.current.assessment } : null, taskId: noProgress.taskId(), budgetTier: taskTier ?? DEFAULT_TASK_TIER, append: record => pi.appendEntry("agent-hub-process-state", record), entries: () => currentCtx?.sessionManager.getEntries() ?? [], }); processState = recovered; processPersistenceBlocked = false; resolveIncomingCapabilities(""); applyWorkModeTools(); return true; } catch { return false; } }, handleTaskTriageWaive: async (args, ctx) => { const [id, ...words] = args.trim().split(/\s+/); const addition = processState.additions?.find(a => a.id === id && a.status === "active" && a.taskId === noProgress.taskId()); if (!addition || !words.length || processBlock()) return false; const next = await confirmTaskTriageWaiver(processState, { taskId: addition.taskId, evaluationId: addition.evaluationId, inputRevision: addition.inputRevision, additionId: id, reason: words.join(" ") }, { taskId: () => noProgress.taskId(), inputRevision: () => taskTriage?.current?.revision ?? "", ask: (nonce, question, askCtx, signal) => requestRuntimeAsk(pi.events, nonce, question, askCtx, signal), startWait: nonce => executionHistory.startAskUser(nonce), endWait: (nonce, sameTask) => { const wait = executionHistory.endAskUser(nonce, Date.now()); if (sameTask && wait > 0) { taskClock = addTaskClockWait(taskClock, wait); turnBudgetAskUserWaitMs += wait; } }, persist: value => pi.appendEntry("agent-hub-process-state", processAuditRecord(value, evaluateProcessObligations(value, { writable: true, budgetTier: taskTier ?? DEFAULT_TASK_TIER }))), }, ctx); if (next) processState = next; return !!next; }, handleAudit: async (ctx, args = "") => { const input = proactiveReportInput(); await showSessionAudit(ctx, sessionDir, input && { ...input, labels: commandProactiveLabels(sessionDir, args) }, { ...taskTriageMetadata(), activity: taskTriageActivity?.live(), }); }, handleRetry: async (args, ctx) => { const dispatchId = args?.trim(); if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}$/.test(dispatchId)) { ctx.ui.notify("Usage: /af-retry ", "error"); return; } const op = noProgress.byDispatch(dispatchId), attempt = op?.attempts.find(a => a.dispatchId === dispatchId); if (!op || !attempt) { ctx.ui.notify("Unknown dispatch ID; no retry authorized.", "error"); return; } await commandCtx.handleRecover(`retry ${op.operationId} ${attempt.attemptId}`, ctx); }, handleRecover: async (args, ctx) => { await runRecoverCommand(args, ctx, { noProgress, currentRevision: cwd => worktreeRevision(cwd, []), confirm: confirmRecoverAction, askPorts: (op, attempt, action) => ({ taskId: () => noProgress.taskId(), language: () => userLanguage, ask: (id: string, params: any, askCtx: any, signal: AbortSignal) => requestRuntimeAsk(pi.events, id, params, askCtx, signal), startWait: (id: string) => executionHistory.startAskUser(id), endWait: (id: string, sameTask: boolean) => { const wait = executionHistory.endAskUser(id, Date.now()); if (sameTask && wait > 0) { taskClock = addTaskClockWait(taskClock, wait); turnBudgetAskUserWaitMs += wait; } }, valid: () => { const current = noProgress.inspect(op.operationId); return current?.attempts.at(-1)?.attemptId === attempt.attemptId && (action === "abandon" ? !current.abandoned && noProgress.isIdle(op.executor) : noProgress.canAuthorize(op.operationId, attempt.attemptId)); }, consume: (nonce: string) => action === "abandon" ? noProgress.abandon(op.operationId, attempt.attemptId, nonce) : attempt.category === "indeterminate" ? noProgress.authorizeIndeterminate(op.operationId, attempt.attemptId, nonce) : noProgress.authorize(attempt.dispatchId), }), }); }, handleAgentsTeam: async (_args, ctx) => { widgetCtx = ctx; const teamNames = Object.keys(teams); if (teamNames.length === 0) { ctx.ui.notify("No teams defined in .pi/agents/teams.yaml", "warning"); return; } const options = teamNames.map(name => { const members = teams[name].map(m => displayName(m)); return `${name} — ${members.join(", ")}`; }); const choice = await ctx.ui.select("Select Team", options); if (choice === undefined) return; const idx = options.indexOf(choice); const name = teamNames[idx]; const selected = resolveSessionRoster({ teams, entries: [], explicitRoster: name, availablePersonas: allAgentDefs.map(def => def.name), }); if (!selected.roster) { ctx.ui.notify(`${selected.diagnostic} Fix .pi/agents/teams.yaml or select another team.`, "error"); return; } activateTeam(selected.roster.name); workModePolicy.clearRosterRecovery(); persistActiveRoster(); resolveIncomingCapabilities(""); applyWorkModeTools(); setTimeout(replayDeferredRecoveryInputs, 0); updateWidget(); ctx.ui.setStatus("agent-team", `Team: ${name} (${agentStates.size})`); ctx.ui.notify(`Team: ${name} — ${Array.from(agentStates.values()).map(s => displayName(s.def.name)).join(", ")}`, "info"); }, handleAgentsList: async (_args, _ctx) => { widgetCtx = _ctx; await openFleetDashboard(_ctx); }, handleAgentsHistory: async (_args, ctx) => { widgetCtx = ctx; await openHistory(ctx, executionHistory, () => (activeTeamName ? `Team: ${activeTeamName}` : "Agent Hub")); }, handleAgentsAdd: async (args, ctx) => { widgetCtx = ctx; const names = (args || "").trim().split(/\s+/).filter(Boolean); if (names.length === 0) { const available = allAgentDefs .filter(d => !agentStates.has(d.name.toLowerCase())) .map(d => d.name).sort().join(", ") || "(all personas are already in the team)"; ctx.ui.notify(`Usage: /af-agents-add […]\nNot in the team yet: ${available}`, "info"); return; } const results = names.map(n => rosterAdd(n)); const level = results.some(r => r.ok) ? "info" : "error"; ctx.ui.notify(results.map(r => r.message).join("\n"), level); ctx.ui.setStatus("agent-team", `Native roster: ${activeTeamName || "(none)"}* (${agentStates.size})`); }, handleAgentsDrop: async (args, ctx) => { widgetCtx = ctx; const names = (args || "").trim().split(/\s+/).filter(Boolean); if (names.length === 0) { ctx.ui.notify(`Usage: /af-agents-drop […]\nActive team: ${Array.from(agentStates.values()).map(s => s.def.name).join(", ")}`, "info"); return; } const results = names.map(n => rosterDrop(n)); const level = results.some(r => r.ok) ? "info" : "error"; ctx.ui.notify(results.map(r => r.message).join("\n"), level); ctx.ui.setStatus("agent-team", `Native roster: ${activeTeamName || "(none)"}* (${agentStates.size})`); }, handleAgentsSave: async (args, ctx) => { widgetCtx = ctx; const name = (args || "").trim(); if (!/^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(name)) { ctx.ui.notify("Usage: /af-agents-save (letters, digits, hyphens, underscores)", "error"); return; } const members = Array.from(agentStates.values()).map(s => s.def.name); if (members.length === 0) { ctx.ui.notify("The active team is empty — nothing to save.", "error"); return; } const teamsPath = join(ctx.cwd || process.cwd(), ".pi", "agents", "teams.yaml"); let raw = ""; try { raw = existsSync(teamsPath) ? readFileSync(teamsPath, "utf-8") : ""; } catch {} try { mkdirSync(join(ctx.cwd || process.cwd(), ".pi", "agents"), { recursive: true }); writeFileSync(teamsPath, upsertTeamInYaml(raw, name, members), "utf-8"); } catch (err) { ctx.ui.notify(`Could not write ${teamsPath}: ${err instanceof Error ? err.message : String(err)}`, "error"); return; } teams[name] = members; activeTeamName = name; persistActiveRoster(); ctx.ui.setStatus("agent-team", `Team: ${name} (${agentStates.size})`); ctx.ui.notify(`Team "${name}" saved to .pi/agents/teams.yaml — ${members.join(", ")}`, "info"); }, handleAgentsKill: async (args, ctx) => { widgetCtx = ctx; await researchControls.handleKill(args, ctx); }, handleAgentsRestart: async (args, ctx) => { widgetCtx = ctx; await researchControls.handleRestart(args, ctx); }, handleContext: async (_args, ctx) => { widgetCtx = ctx; await openContextBudget(ctx); }, handleHubReport: async (args, ctx) => { widgetCtx = ctx; const fmtTok = (n: number) => n >= 1_000_000 ? `${(n / 1_000_000).toFixed(1)}M` : n >= 1000 ? `${Math.round(n / 1000)}k` : String(n); const renderReport = (label: string, r: TurnReport): string => { const billed = r.dispatches.reduce((n, d) => n + d.billed, 0); const out = r.dispatches.reduce((n, d) => n + d.out, 0); const rows = r.dispatches.map(d => ` ${d.agent}: ${d.status} in ${Math.round(d.elapsed / 1000)}s · ${fmtTok(d.billed)} billed / ${fmtTok(d.out)} out`); return [ `${label} — tier ${r.tier ?? "(unset)"} · ${r.dispatches.length} dispatch(es) · ${r.research} research · ` + `${fmtTok(billed)} billed / ${fmtTok(out)} out · ${r.recycles} recycle(s) · ${r.driftStops} drift stop(s) · ${r.refusals} refusal(s)`, ...rows, ].join("\n"); }; const lines: string[] = []; if (turnReport.dispatches.length > 0 || turnReport.research > 0 || turnReport.refusals > 0) { lines.push(renderReport("Current turn", turnReport)); } if (lastTurnReport) lines.push(renderReport("Last turn", lastTurnReport)); lines.push( `Session — ${sessionTotals.turns} dispatching turn(s) · ${sessionTotals.dispatches} dispatch(es) · ${sessionTotals.research} research · ` + `${fmtTok(sessionTotals.billed)} billed / ${fmtTok(sessionTotals.out)} out · ${sessionTotals.recycles} recycle(s) · ` + `${sessionTotals.driftStops} drift stop(s) · ${sessionTotals.refusals} refusal(s)`, ); const sweep = unaddressedPeerSweep(Array.from(hubSpawnedPeers.values())); if (sweep) lines.push(sweep.message); const trace = readWatchdogEvents(sessionDir); lines.push(`Watchdog System 1 — ${JSON.stringify(buildWatchdogReport(trace.events, watchdogActivity?.live(), trace.integrity))}`); const proactiveInput = proactiveReportInput(); lines.push(`Proactive review — ${JSON.stringify(proactiveInput ? buildProactiveReport({ ...proactiveInput, labels: commandProactiveLabels(sessionDir, args) }) : readProactiveReport(sessionDir))}`); ctx.ui.notify(lines.join("\n\n"), "info"); }, handleZoom: async (args, ctx) => { widgetCtx = ctx; const arg = args?.trim() || ""; const rid = parseResearchHandle(arg); const target: Zoomable | undefined = rid != null ? researchStates.get(rid) : arg ? agentStates.get(arg.toLowerCase()) ?? findDelegationChild(arg)?.child : undefined; if (!target) { const teamKnown = Array.from(agentStates.values()).map(s => displayName(s.def.name)).join(", "); const researchKnown = Array.from(researchStates.values()).map(s => `r${s.id}`).join(", "); const childKnown = Array.from(agentStates.values()) .flatMap(s => Array.from(s.delegations?.keys() || [])).join(", "); const known = [teamKnown, researchKnown, childKnown].filter(Boolean).join(", "); ctx.ui.notify(`Usage: /af-zoom . Known: ${known || "none"}`, "error"); return; } const rowKey = (rid != null ? `r${rid}` : arg).toLowerCase(); const row = fleetRows(true).find(r => r.key.toLowerCase() === rowKey); if (row) await openFleetDetail(row, ctx); else await openZoom(target, ctx); }, handleDispatchPolicy: async (_args, ctx) => { widgetCtx = ctx; const live = new Set(comsReady && identity ? peersInScope().map(e => e.name.toLowerCase()) : []); const lines = Array.from(agentStates.values()).map(s => { const key = s.def.name.toLowerCase(); const sub = dispatchPolicy.substitutions[key]; const prefer = sub ? sub.prefer : dispatchPolicy.default === "coms" ? "coms" : "native"; if (prefer !== "coms") return `${displayName(s.def.name)}: native`; const fb = sub?.fallback === "none" ? "coms-required" : "coms, fallback native"; return `${displayName(s.def.name)}: ${fb} — peer ${live.has(key) ? "LIVE" : "not in pool"}`; }); const policyPath = join(ctx.cwd || process.cwd(), ".pi", "agents", "dispatch-policy.yaml"); const src = existsSync(policyPath) ? ".pi/agents/dispatch-policy.yaml" : "(no dispatch-policy.yaml — all native)"; ctx.ui.notify( `Dispatch backends — ${src}, default: ${dispatchPolicy.default}\n${lines.join("\n") || "(no active team)"}\n` + `Routing is decided per dispatch against the live coms pool (/af-coms to refresh).`, "info", ); }, handleAgentModel: async (args, ctx) => { widgetCtx = ctx; const arg = (args || "").trim().toLowerCase(); // Dot form: . targets a delegate sub-role. Candidates are // the role's declared model (default) + the parent's candidate list. if (arg.includes(".")) { const dot = arg.indexOf("."); const personaName = arg.slice(0, dot); const roleName = arg.slice(dot + 1); const parent = agentStates.get(personaName); const roles = parent?.def.subagents || {}; const roleKey = Object.keys(roles).find(r => r.toLowerCase() === roleName); if (!parent || !roleKey) { const valid = Array.from(agentStates.values()).flatMap(s => Object.keys(s.def.subagents || {}).map(r => `${s.def.name}.${r}`)); ctx.ui.notify( `No sub-role "${arg}". Valid targets: ${valid.join(", ") || "none (no persona declares subagents:)"}`, "error", ); return; } const role = roles[roleKey]; const overrideKey = `${personaName}.${roleKey.toLowerCase()}`; const candidates: string[] = []; for (const m of [role.model, ...allowedModels(parent.def)]) { if (m && !candidates.includes(m)) candidates.push(m); } const current = resolvedSubagentModel(parent.def.name, roleKey, role.model); const options = candidates.map(m => { const tags = [m === role.model ? "default" : "", m === current ? "current" : ""].filter(Boolean); return tags.length ? `${m} (${tags.join(", ")})` : m; }); const label = `${displayName(parent.def.name)}.${roleKey}`; const choice = await ctx.ui.select(`Model for ${label}`, options); if (choice === undefined) return; const picked = candidates[options.indexOf(choice)]; const effectivePicked = substitutedModel(picked) ?? picked; if (effectivePicked === current) { ctx.ui.notify(`${label} is already on ${effectivePicked}`, "info"); return; } if (picked === role.model) { modelPolicy.setSubagentOverride(personaName, roleKey, undefined); } else { modelPolicy.setSubagentOverride(personaName, roleKey, picked); } updateWidget(); ctx.ui.notify( `${label} → ${effectivePicked} (applies on next dispatch of ${parent.def.name})`, "info", ); return; } const name = arg; // Team member (live state) OR a research persona — both switchable. const def = name ? switchablePersonaDef(name) : undefined; if (!def) { const known = [ ...Array.from(agentStates.values()).map(s => s.def.name), ...researchPersonas.map(d => d.name), ].join(", "); ctx.ui.notify(`Usage: /af-agent-model [.]. Known: ${known || "none"}`, "error"); return; } if (!def.models || def.models.length === 0) { ctx.ui.notify( `${displayName(def.name)} declares no model candidates — add a \`models:\` list to ${def.file} or a \`models.${def.name}:\` override in .ai/agent-fleet-overrides.md.`, "warning", ); return; } const candidates = allowedModels(def); const current = resolvedModel(def); // A persona without a frontmatter default runs on the dispatcher's model — // offer that as an explicit candidate so the override can be cleared. const DISPATCHER_DEFAULT = "(dispatcher's model)"; if (!def.model) candidates.unshift(DISPATCHER_DEFAULT); const options = candidates.map(m => { const isDefault = def.model ? m === def.model : m === DISPATCHER_DEFAULT; const isCurrent = current ? m === current : m === DISPATCHER_DEFAULT; const tags = [isDefault ? "default" : "", isCurrent ? "current" : ""].filter(Boolean); return tags.length ? `${m} (${tags.join(", ")})` : m; }); const choice = await ctx.ui.select(`Model for ${displayName(def.name)}`, options); if (choice === undefined) return; const picked = candidates[options.indexOf(choice)]; const effectivePicked = picked === DISPATCHER_DEFAULT ? picked : (substitutedModel(picked) ?? picked); const pickedIsCurrent = current ? effectivePicked === current : picked === DISPATCHER_DEFAULT; if (pickedIsCurrent) { ctx.ui.notify(`${displayName(def.name)} is already on ${effectivePicked}`, "info"); return; } if (picked === def.model || picked === DISPATCHER_DEFAULT) { modelPolicy.setPersonaOverride(name, undefined); } else { modelPolicy.setPersonaOverride(name, picked); } updateWidget(); // Research helpers spawn fresh each time, so the switch lands on their // next spawn; team members apply on next dispatch (restartable now). const applyHint = (def.kind || "").toLowerCase() === "research" ? "applies on next spawn_research" : `applies on next dispatch; /af-agents-restart ${def.name} to apply now`; ctx.ui.notify(`${displayName(def.name)} → ${effectivePicked} (${applyHint})`, "info"); if ((dispatchPolicy.substitutions[name]?.prefer ?? dispatchPolicy.default) === "coms") { ctx.ui.notify( `Note: ${displayName(def.name)} prefers a coms peer (dispatch-policy.yaml) — this model override only applies to native(-fallback) runs; the peer keeps its own model.`, "info", ); } }, handleAgentModelThinking: async (args, ctx) => { widgetCtx = ctx; const name = (args || "").trim().toLowerCase(); // Team member (live state) OR a research persona — both switchable. const def = name ? switchablePersonaDef(name) : undefined; if (!def) { const known = [ ...Array.from(agentStates.values()).map(s => s.def.name), ...researchPersonas.map(d => d.name), ].join(", "); ctx.ui.notify(`Usage: /af-agent-model-thinking . Known: ${known || "none"}`, "error"); return; } const defaultLevel = resolveThinkingLevel(def.thinking); const current = resolveThinkingLevel(resolvedThinking(def)); const levels = [...THINKING_LEVELS]; const options = levels.map(l => { const tags = [l === defaultLevel ? "default" : "", l === current ? "current" : ""].filter(Boolean); return tags.length ? `${l} (${tags.join(", ")})` : l; }); const choice = await ctx.ui.select(`Thinking level for ${displayName(def.name)}`, options); if (choice === undefined) return; const picked = levels[options.indexOf(choice)]; if (picked === current) { ctx.ui.notify(`${displayName(def.name)} is already on thinking: ${picked}`, "info"); return; } if (picked === defaultLevel) { modelPolicy.setThinkingOverride(name, undefined); } else { modelPolicy.setThinkingOverride(name, picked); } updateWidget(); // Research helpers spawn fresh each time, so the switch lands on their // next spawn; team members apply on next dispatch (restartable now). const applyHint = (def.kind || "").toLowerCase() === "research" ? "applies on next spawn_research" : `applies on next dispatch; /af-agents-restart ${def.name} to apply now`; ctx.ui.notify(`${displayName(def.name)} thinking → ${picked} (${applyHint})`, "info"); }, handleModels: async (args, ctx) => { widgetCtx = ctx; const names = Object.keys(modelProfiles); if (names.length === 0) { ctx.ui.notify("No model profiles loaded — define .pi/agents/model-profiles.yaml (invalid profiles are dropped at session start).", "warning"); return; } let profileName = (args || "").trim(); if (!profileName) { const options = names.map(n => isCompleteProfile(modelProfiles[n]) ? `${n} — complete fleet (${shortModel((modelProfiles[n] as any).defaults.model)} default)` : `${n} — ${Object.entries(modelProfiles[n]).map(([p, m]) => `${p}: ${shortModel(m)}`).join(", ")}`, ); const choice = await ctx.ui.select("Select model profile", options); if (choice === undefined) return; profileName = names[options.indexOf(choice)]; } const profile = modelProfiles[profileName]; if (!profile) { ctx.ui.notify(`No profile "${profileName}". Known: ${names.join(", ")}`, "error"); return; } profileCommandContext=ctx; try { const applied=await profileActivation.activate(profileName,profile); applyWorkModeTools(); ctx.ui.setStatus('hub-model-profile',isCompleteProfile(profile)?`Models: ${profileName}`:undefined); ctx.ui.notify(`Profile "${profileName}": ${applied.length} personas${isCompleteProfile(profile)?', all children, dispatcher and auxiliary models':''} switched.`, 'info'); } catch(error) {ctx.ui.notify(String(error),'error');} }, handleAgentModelsSubstitute: async (args, ctx) => { widgetCtx = ctx; const tokens = (args || "").trim().split(/\s+/).filter(Boolean); if (tokens.length === 0) { if (allKnownModels().length === 0) { ctx.ui.notify("No configured persona or sub-role models are available as substitution sources.", "warning"); return; } await openFleetDashboard(ctx, true); return; } if (tokens.length !== 2) { ctx.ui.notify("Usage: /af-agent-models-substitute [ ]", "error"); return; } await applySessionModelSubstitution(tokens[0], tokens[1], ctx); }, handleWatchdog: async (args, ctx) => { widgetCtx = ctx; const parts = (args || "").trim().split(/\s+/).filter(Boolean); if (parts.length === 0) { const perAgent = watchdogAgentOverrides.size > 0 ? Array.from(watchdogAgentOverrides.entries()).map(([k, v]) => `${k}: ${v}`).join(", ") : "(none)"; ctx.ui.notify( `Drift watchdog: ${watchdogSetting} (hub-wide)\nPer-agent overrides: ${perAgent}\n` + `Judge model: ${watchdogJudgeModel || "(researcher persona's, else dispatcher's)"}\n` + `${formatWatchdogStatus(watchdogSystem1, watchdogActivity)}\n` + `Usage: /af-watchdog on|off|auto — or /af-watchdog on|off|clear`, "info", ); return; } if (parts.length === 1) { const setting = normalizeWatchdogSetting(parts[0]); if (!setting) { ctx.ui.notify(`Unknown setting "${parts[0]}" — expected one of: ${WATCHDOG_SETTINGS.join(", ")} (or /af-watchdog on|off|clear)`, "error"); return; } watchdogSetting = setting; ctx.ui.notify(`Drift watchdog → ${setting} (applies from the next dispatch)`, "info"); return; } const agentKey = normalizeAgentInput(parts[0]); const value = parts[1].toLowerCase(); if (!agentStates.has(agentKey)) { ctx.ui.notify(`"${parts[0]}" is not in the active team (${Array.from(agentStates.values()).map(s => s.def.name).join(", ")})`, "error"); return; } if (value === "clear") { watchdogAgentOverrides.delete(agentKey); ctx.ui.notify(`Drift watchdog override cleared for ${agentKey} (hub-wide setting "${watchdogSetting}" applies)`, "info"); return; } if (value !== "on" && value !== "off") { ctx.ui.notify(`Per-agent watchdog must be on, off, or clear — got "${parts[1]}"`, "error"); return; } watchdogAgentOverrides.set(agentKey, value); ctx.ui.notify(`Drift watchdog for ${agentKey} → ${value} (overrides the hub-wide "${watchdogSetting}")`, "info"); }, handleComs: async (args, ctx) => { if (!comsReady) { ctx.ui.notify("coms is not active in this session.", "warning"); return; } await coms.updateScope((args ?? "").trim(), ctx); }, handleHandoff: async (args, ctx) => { if (modelWorkBlockedByRosterRecovery(ctx)) return; if (!comsReady) { ctx.ui.notify("coms is not active in this session — /af-handoff unavailable.", "warning"); return; } const target = (args ?? "").trim(); if (!target) { ctx.ui.notify("Usage: /af-handoff . See the coms pool for live peer names.", "error"); return; } const peer = resolveTarget(target); if (!peer) { ctx.ui.notify(`coms: no live peer "${target}". Use /af-coms to refresh the pool.`, "error"); return; } const refusal=profilePeerGate({ peerModel: peer.model, targetResolved: true });if(refusal){ctx.ui.notify(refusal.content[0].text,"error");return;} const handoffToken = crypto.randomBytes(8).toString("hex"); hubStateCtx.setPendingHandoff({ target: peer.name, token: handoffToken }); pi.sendMessage({ customType: "coms-handoff", content: `HANDOFF REQUEST → peer "${peer.name}".\n\n` + `Compose a SELF-CONTAINED handoff brief (the peer does NOT share your context): state the ` + `overall goal, what's been done so far, key decisions and constraints, the current status, ` + `and the concrete next steps you want the peer to take. Then call ` + `coms_send(target: "${peer.name}", handoff_token: "${handoffToken}", prompt: ), coms_await its msg_id, and relay ` + `the peer's reply to me in ${userLanguage}.`, display: true, }, { deliverAs: "followUp", triggerTurn: true }); ctx.ui.notify(`Handoff to ${peer.name}: asking the dispatcher to compose a brief…`, "info"); }, handleCompound: async (args, ctx) => { if (modelWorkBlockedByRosterRecovery(ctx)) return; if (!agentStates.has("documenter")) { ctx.ui.notify( "compound: the documenter persona is not in the active team — switch with /af-agents-team (e.g. default or release), then re-run /af-compound.", "warning", ); return; } const focus = (args ?? "").trim(); const rulesLine = projectRulesDirs.length > 0 ? projectRulesDirs.join(", ") : "(none declared in .ai/agent-fleet-overrides.md — the documenter must locate an existing rules tree or, failing that, propose lessons without writing)"; const docsLine = projectDocsPaths.length > 0 ? projectDocsPaths.join(", ") : "(none declared)"; pi.sendMessage({ customType: "compound-learning", content: `COMPOUND REQUEST — capture this session's lessons into the project's rules and docs (compound-learning pass).\n\n` + `1. From THIS session's context, compose a candidate-lessons brief: user corrections, review findings that recurred, ` + `wrong assumptions that cost rework, debugging root causes, and changes that invalidated existing docs. At most 5 lessons; ` + `each is one imperative sentence plus a one-line Why (the failure it prevents) and a one-line Evidence (what happened this session). ` + `${focus ? `Focus especially on: ${focus}. ` : ""}` + `If nothing rises to a lesson, tell the user there is nothing worth compounding and stop.\n` + `2. Confirm the list with the user in ${userLanguage} (ask_user when available) — they approve, trim, or reword. Do not dispatch before this confirmation.\n` + `3. Dispatch the documenter with a SELF-CONTAINED task (it shares none of your context) containing: the approved lessons verbatim ` + `(with Why + Evidence); the project rule folders: ${rulesLine}; the docs entry points: ${docsLine}; the assertion ledger path ` + `.pi/agent-sessions/assertions.json (when it exists); and the instruction to read skills/compound-learning/SKILL.md and follow it exactly — ` + `dedupe index-first against the existing rule tree, minimal diffs on existing files, caps of 5 lessons / 1 new file. State that the user ` + `already approved this lesson list, so it may apply without a second gate. Pass the relevant review/return/evidence artifact paths via the ` + `dispatch's artifacts array — paths only, never pasted bodies.\n` + `4. Relay the documenter's file-by-file result to me in ${userLanguage}.`, display: true, }, { deliverAs: "followUp", triggerTurn: true }); ctx.ui.notify("Compound: asking the dispatcher to compose the candidate-lessons brief…", "info"); }, handlePoll: async (args, ctx) => { return withProfileWork(async () => { if (modelWorkBlockedByRosterRecovery(ctx)) return; const cwd = ctx.cwd || process.cwd(); const result = await handleAfPoll({ args: args ?? "", cwd, pollPanelOverride: readActiveProfile()?.name ?? parseAgentTeamOverrides(cwd).pollPanel, listPanels: async dir => { const { listPanelNames } = await import("../../agent-fleet/scripts/workflows/lib/voices.ts"); return listPanelNames(dir); }, preflight: async ({ panel, persona, cwd: root }) => { const [{ resolvePersona }, { resolvePanel }, { checkChildVisibility }] = await Promise.all([ import("../../agent-fleet/scripts/workflows/lib/personas.ts"), import("../../agent-fleet/scripts/workflows/lib/voices.ts"), import("../../agent-fleet/scripts/workflows/lib/model-visibility.ts"), ]); try { resolvePersona(persona, root); } catch (error) { return error instanceof Error ? error.message : String(error); } let voices; try { voices = resolvePanel(panel, root); } catch (error) { return error instanceof Error ? error.message : String(error); } const report = checkChildVisibility(voices.map(voice => voice.model)); if (report.diagnostic) return `could not verify clean-room model visibility (${report.diagnostic})`; const hidden = report.models.filter(model => !model.ok); if (hidden.length) { return `panel "${panel}" has models not visible to a clean-room child: ${hidden.map(model => model.reasons[0] ?? model.model).join("; ")}`; } return null; }, checkBudget: async () => { ensureTaskTier(); const blocked = await budgetRecovery.ensure("dispatch", args ?? "model panel", ctx); return blocked?.message ?? null; }, chargeBudget: () => { turnDispatchCount += 1; taskDispatchCount += 1; sessionTotals.dispatches += 1; updateModeStatus(); }, onAccepted: async ({ panel, persona, question }) => { let voices: { name: string; model: string }[] = []; try { const { resolvePanel } = await import("../../agent-fleet/scripts/workflows/lib/voices.ts"); voices = resolvePanel(panel, cwd).map(voice => ({ name: voice.name, model: voice.model })); } catch { /* names are decorative */ } const started = formatAfPollStarted({ panel, persona, question, voices }); ctx.ui.setStatus("hub-poll", `Poll: ${panel} (${voices.length || "?"} voices) running…`); ctx.ui.notify(`Poll started — panel ${panel}. This can take a minute.`, "info"); pi.sendMessage({ customType: "af-poll-started", content: started, display: true }, { deliverAs: "followUp", triggerTurn: false }); }, execute: async ({ panel, persona, question }) => { const [{ Run }, { runPoll }, { runMerge }, { resolvePersona }] = await Promise.all([ import("../../agent-fleet/scripts/workflows/lib/run.ts"), import("../../agent-fleet/scripts/workflows/lib/poll.ts"), import("../../agent-fleet/scripts/workflows/lib/merge.ts"), import("../../agent-fleet/scripts/workflows/lib/personas.ts"), ]); const run = new Run({ cwd, command: ["/af-poll", "--panel", panel, question] }); const personaDef = resolvePersona(persona, cwd); const poll = await runPoll({ run, cwd, persona: personaDef, panel, task: question, onVoice: result => { const voice = result.ok ? { name: result.voice.name, model: result.voice.model, ok: true as const, position: result.report.position, confidence: result.report.confidence } : { name: result.voice.name, model: result.voice.model, ok: false as const, reason: result.reason }; pi.sendMessage({ customType: "af-poll-voice", content: formatAfPollVoiceProgress(voice), display: true }, { deliverAs: "followUp", triggerTurn: false }); }, }); ctx.ui.setStatus("hub-poll", `Poll: ${panel} merging…`); const merge = await runMerge({ run, cwd, persona: personaDef, panel, task: question, opinions: poll.results }); const directory = path.relative(cwd, poll.directory) || poll.directory; return { panel, directory: directory.endsWith(path.sep) ? directory : `${directory}${path.sep}`, voices: poll.results.map(item => item.ok ? { name: item.voice.name, model: item.voice.model, ok: true as const, position: item.report.position, confidence: item.report.confidence } : { name: item.voice.name, model: item.voice.model, ok: false as const, reason: item.reason }), recommendation: merge.report.recommendation, integrator: merge.integrator.name, }; }, }); ctx.ui.setStatus("hub-poll", ""); if (!result.ok) { ctx.ui.notify(result.message, "error"); pi.sendMessage({ customType: "af-poll-failed", content: `POLL FAILED\n\n${result.message}`, display: true }, { deliverAs: "followUp", triggerTurn: false }); return; } ctx.ui.notify(result.digest ?? result.message, "info"); pi.sendMessage({ customType: "af-poll", content: result.dispatcherNote ?? result.message, display: true, }, { deliverAs: "followUp", triggerTurn: true }); }); }, handleDebate: async (args, ctx) => { return withProfileWork(async () => { if (modelWorkBlockedByRosterRecovery(ctx)) return; const cwd = ctx.cwd || process.cwd(); const result = await handleAfDebate({ args: args ?? "", cwd, pollPanelOverride: readActiveProfile()?.name ?? parseAgentTeamOverrides(cwd).pollPanel, listPanels: async dir => { const { listPanelNames } = await import("../../agent-fleet/scripts/workflows/lib/voices.ts"); return listPanelNames(dir); }, preflight: async ({ panel, persona, cwd: root }) => { const [{ resolvePersona }, { resolvePanel }, { checkChildVisibility }] = await Promise.all([ import("../../agent-fleet/scripts/workflows/lib/personas.ts"), import("../../agent-fleet/scripts/workflows/lib/voices.ts"), import("../../agent-fleet/scripts/workflows/lib/model-visibility.ts"), ]); try { resolvePersona(persona, root); } catch (error) { return error instanceof Error ? error.message : String(error); } let voices; try { voices = resolvePanel(panel, root); } catch (error) { return error instanceof Error ? error.message : String(error); } const report = checkChildVisibility(voices.map(voice => voice.model)); if (report.diagnostic) return `could not verify clean-room model visibility (${report.diagnostic})`; const hidden = report.models.filter(model => !model.ok); if (hidden.length) { return `panel "${panel}" has models not visible to a clean-room child: ${hidden.map(model => model.reasons[0] ?? model.model).join("; ")}`; } return null; }, checkBudget: async () => { ensureTaskTier(); const blocked = await budgetRecovery.ensure("dispatch", args ?? "model panel", ctx); return blocked?.message ?? null; }, chargeBudget: () => { turnDispatchCount += 1; taskDispatchCount += 1; sessionTotals.dispatches += 1; updateModeStatus(); }, onAccepted: async ({ panel, persona, question, rounds }) => { let voices: { name: string; model: string }[] = []; try { const { resolvePanel } = await import("../../agent-fleet/scripts/workflows/lib/voices.ts"); voices = resolvePanel(panel, cwd).map(voice => ({ name: voice.name, model: voice.model })); } catch { /* names are decorative */ } const started = formatAfDebateStarted({ panel, persona, question, rounds, voices }); ctx.ui.setStatus("hub-poll", `Debate: ${panel} (${rounds} rounds) running…`); ctx.ui.notify(`Debate started — panel ${panel}, ${rounds} rounds.`, "info"); pi.sendMessage({ customType: "af-debate-started", content: started, display: true }, { deliverAs: "followUp", triggerTurn: false }); }, execute: async ({ panel, persona, question, rounds }) => { const [{ Run }, { runDebate }, { resolvePersona }] = await Promise.all([ import("../../agent-fleet/scripts/workflows/lib/run.ts"), import("../../agent-fleet/scripts/workflows/lib/debate.ts"), import("../../agent-fleet/scripts/workflows/lib/personas.ts"), ]); const run = new Run({ cwd, command: ["/af-debate", "--panel", panel, "--rounds", String(rounds), question] }); const personaDef = resolvePersona(persona, cwd); const debate = await runDebate({ run, cwd, persona: personaDef, panel, task: question, rounds, onVoice: (result, round) => { const voice = result.ok ? { name: result.voice.name, model: result.voice.model, ok: true as const, round, position: result.report.position, changed: result.report.changed } : { name: result.voice.name, model: result.voice.model, ok: false as const, round, reason: result.reason }; ctx.ui.setStatus("hub-poll", `Debate: ${panel} round ${round}…`); pi.sendMessage({ customType: "af-debate-voice", content: formatAfDebateVoiceProgress(voice), display: true }, { deliverAs: "followUp", triggerTurn: false }); }, }); const last = debate.roundsRun.at(-1)?.results ?? []; const directory = path.relative(cwd, debate.directory) || debate.directory; return { panel, rounds: debate.rounds, directory: directory.endsWith(path.sep) ? directory : `${directory}${path.sep}`, voices: last.map(item => item.ok ? { name: item.voice.name, model: item.voice.model, ok: true as const, round: item.round, position: item.report.position, changed: item.report.changed } : { name: item.voice.name, model: item.voice.model, ok: false as const, round: item.round, reason: item.reason }), }; }, }); ctx.ui.setStatus("hub-poll", ""); if (!result.ok) { ctx.ui.notify(result.message, "error"); pi.sendMessage({ customType: "af-debate-failed", content: `DEBATE FAILED\n\n${result.message}`, display: true }, { deliverAs: "followUp", triggerTurn: false }); return; } ctx.ui.notify(result.digest ?? result.message, "info"); pi.sendMessage({ customType: "af-debate", content: result.dispatcherNote ?? result.message, display: true, }, { deliverAs: "followUp", triggerTurn: true }); }); }, getAgentsKillCompletions: prefix => completions.agentsKill(prefix), getZoomCompletions: prefix => completions.zoom(prefix), getAgentModelCompletions: prefix => completions.agentModels(prefix), getAgentModelThinkingCompletions: prefix => completions.agentThinking(prefix), getModelProfileCompletions: prefix => completions.modelProfiles(prefix), getSubstituteCompletions: prefix => completions.substitutions(prefix), getComsPeerCompletions: prefix => completions.comsPeers(prefix), getSubagentTargetCompletions: prefix => completions.subagentTargets(prefix), }; // Keep the complete command surface flat and greppable in this composition root. registerAgentsTeam(pi, commandCtx); registerAgentsList(pi, commandCtx); registerAgentsHistory(pi, commandCtx); registerContextCommand(pi, commandCtx); registerAudit(pi, commandCtx); registerWorkMode(pi, commandCtx); registerWatchdog(pi, commandCtx); registerAgentsAdd(pi, commandCtx); registerAgentsDrop(pi, commandCtx); registerAgentsSave(pi, commandCtx); registerHubReport(pi, commandCtx); registerZoom(pi, commandCtx); registerAgentModel(pi, commandCtx); registerAgentModelThinking(pi, commandCtx); registerModels(pi, commandCtx); registerAgentModelsSubstitute(pi, commandCtx); registerDispatchPolicy(pi, commandCtx); registerAgentsKill(pi, commandCtx); registerAgentsRestart(pi, commandCtx); registerComs(pi, commandCtx); registerHandoff(pi, commandCtx); registerCompound(pi, commandCtx); registerPoll(pi, commandCtx); registerBudgetContinue(pi, commandCtx); registerRetry(pi, commandCtx); registerRecover(pi, commandCtx); registerDebate(pi, commandCtx); registerTaskTriageRecover(pi, commandCtx); registerTaskTriageWaiver(pi, commandCtx); const detailPanel = createDetailPanel({ getAgent: key => agentStates.get(key), getResearch: id => researchStates.get(id), parseResearchHandle, findDelegationChild, modelPolicy, displayName, shortModel, refreshUi: updateWidget, getDispatchPreference: name => (dispatchPolicy.substitutions[name.toLowerCase()]?.prefer ?? dispatchPolicy.default) === "coms" ? "coms" : "native", maxLiveEntryChars: MAX_LIVE_ENTRY_CHARS, getProactive: () => fleetSource.snapshot(Date.now()).proactive, readProactiveEvidence: (finding) => proactiveRuntime?.findings.readback(finding.snapshotHandle, finding.snapshotHash, finding.snapshotId, finding.unitId, finding.excerptHash) ?? null, currentFleetRow: key => fleetSource.rows(Date.now(), { showFinished: true }).find(row => row.key === key), }); const { openFleetDetail, loadAvailableModelChoices } = detailPanel; const applySessionModelSubstitution = (source: string, target: string, ctx: any) => modelPolicy.applySessionSubstitution(source, target, { loadAvailable: current => loadAvailableModelChoices(ctx, current), notify: (message, level) => ctx.ui.notify(message, level), }); fleetActions = createFleetActions({ getRows: () => fleetSource.rows(Date.now(), { showFinished: true }), getAgents: () => agentStates, getResearch: () => researchStates, parseResearchHandle, displayName, modelWorkBlocked: modelWorkBlockedByRosterRecovery, restartSpecialist: researchControls.restartSpecialist, removeResearch: researchControls.remove, killSpecialistProcess: state => { fenceOperatorCancel(state); cancelLocalOwnedProcess({ process: state.proc, monitorBridge, monitorKey: monitorKeyForAgent(state.def.name, state.dispatchId ?? state.runCount), treeKill: killPiTree }); }, abortComs: state => { state.comsAbort?.(); }, openDetail: openFleetDetail, generation: () => fleetUiGeneration, }); let fleetShowFinished = false; let fleetFilter = ""; const fleetDashboard = createFleetDashboard({ onThinkingChanged: updateWidget, openSystem1: ctx => openSystem1Communication(ctx, communicationStore), getFleetRows: (now, unfiltered) => fleetSource.rows(now, unfiltered ? { showFinished: true } : { showFinished: fleetShowFinished, query: fleetFilter }), getTaskTriage: () => fleetSource.snapshot(Date.now()).taskTriage, getProactive: () => fleetSource.snapshot(Date.now()).proactive, readProactiveEvidence: (finding) => proactiveRuntime?.findings.readback(finding.snapshotHandle, finding.snapshotHash, finding.snapshotId, finding.unitId, finding.excerptHash) ?? null, actions: fleetActions, getAgents: () => agentStates, getResearch: () => researchStates, getShowFinished: () => fleetShowFinished, setShowFinished: value => { fleetShowFinished = value; }, getFilter: () => fleetFilter, setFilter: value => { fleetFilter = value; }, parseResearchHandle, displayName, shortModel, thinkingSuffix, modelWithThinking, resolvedThinking, abbreviatePeerModel: abbreviateModel, modelPolicy, loadAvailableModels: loadAvailableModelChoices, openDetail: openFleetDetail, openHistory: ctx => openHistory(ctx, executionHistory, () => (activeTeamName ? `Team: ${activeTeamName}` : "Agent Hub")), modelWorkBlocked: modelWorkBlockedByRosterRecovery, restartSpecialist: researchControls.restartSpecialist, removeResearch: researchControls.remove, killSpecialistProcess: state => { fenceOperatorCancel(state); cancelLocalOwnedProcess({ process: state.proc, monitorBridge, monitorKey: monitorKeyForAgent(state.def.name, state.dispatchId ?? state.runCount), treeKill: killPiTree }); }, abortComs: state => { state.comsAbort?.(); }, getComsLines: (width, theme) => poolPresentation.render(width, theme), }); const { fleetRows, openFleetDashboard: openFleetDashboardRaw } = fleetDashboard; const openFleetDashboard = (ctx: any, startSubstitution = false) => gridUI.withSuspended(() => openFleetDashboardRaw(ctx, startSubstitution)); const contextBudgetUi = createContextBudgetUi({ getAgents: () => agentStates, getResearch: () => researchStates, getAllDefs: () => allAgentDefs, getResearchPersonas: () => researchPersonas, getPeers: () => peerCards.values(), modelPolicy, displayName, getProjectDocsPaths: () => projectDocsPaths, getUserLanguage: () => userLanguage, getDelegateExtensionPath: () => delegateExtPath, safeAgentKey, projectPolicyPaths: specialistProjectPolicyPaths, modelWindowLookup, getResearchTools: () => RESEARCH_TOOLS, getPromptLedger: () => lastHubLedger, getPressureState: () => contextPressureState, buildHubSystemPrompt: () => { buildHubSystemPrompt(); }, getAllTools: () => typeof pi.getAllTools === "function" ? pi.getAllTools() : [], getActiveTools: () => typeof pi.getActiveTools === "function" ? pi.getActiveTools() : [], getCommands: () => typeof pi.getCommands === "function" ? pi.getCommands() : [], }); const { openContextBudget } = contextBudgetUi; // A delegate child anywhere in the team, by its id (e.g. "quality-1"). function findDelegationChild(arg: string): { child: DelegationChild; owner: AgentState } | null { const lower = arg.toLowerCase(); for (const state of agentStates.values()) { const child = Array.from(state.delegations?.values() ?? []).find(candidate => candidate.id.toLowerCase() === lower); if (child) return { child, owner: state }; } return null; } const completions = createCompletionPresentation({ getAgents: () => agentStates.values(), getResearch: () => researchStates.values(), getResearchPersonas: () => researchPersonas, getModelProfiles: () => modelProfiles, getPeers: peersInScope, displayName, shortModel, resolvedModel, resolvedThinking, resolveThinkingLevel, resolvedSubagentModel, getSubagentOverride: (persona, role) => modelPolicy.getSubagentOverride(persona, role), getSubstitutionSources: () => modelPolicy.allKnownModels().map(spec => { const target = modelPolicy.getSubstitution(spec); return { spec, label: target ? `${spec} → ${target} (active this session)` : spec }; }), }); registerInputShortcuts(pi, { setWidgetContext: ctx => { widgetCtx = ctx; }, openFleetDashboard, toggleFleetWidget: () => gridUI.toggle(), workModeStatusText, openWorkModePicker, }); // Root owns subscription order; lifecycle modules own handler bodies. registerTurnPresence(pi, { beforeAgentPresence: () => turnHandlers.beforeAgentPresence(), agentEndPresence: () => turnHandlers.agentEndPresence(), }); // /af-handoff — hand the session off to a coms peer. Per decision G1 we do NOT // extract the compaction summary; instead we ask the dispatcher LLM (next turn) to // compose a SELF-CONTAINED brief and coms_send it, then await + relay the reply. // /af-compound [focus] — end-of-session compound-learning pass. Mirrors /af-handoff's // shape: the dispatcher LLM (which saw the whole session) composes the // candidate-lessons brief itself, gates it on the user, then dispatches the // documenter to land the approved lessons per skills/compound-learning/SKILL.md. // The rules/docs targets come from the overrides file; artifacts travel as // paths through the dispatch's `artifacts` array, never as pasted bodies. // ── ask_user wait tracking (for /af-agents-history real-work) ── // pi-ask-user blocks the dispatcher turn while the human answers. Bracket each // ask_user call with its tool_execution start/end so /af-agents-history can subtract // that "away from keyboard" time from the dispatcher's real work. // T11 production pre-effect gate: operator direct side effects use the same task-scoped process state as child dispatch. pi.on("tool_call", async (event: any, ctx) => { const toolName = String(event.toolName || "").toLowerCase(); const block = processBlock(); // set_task_tier binds the fence. ask_user is the human supersession path. Every other tool is a diversion. // The first assistant step may emit several tools together. Refuse that whole step without aborting, // so the model can still call set_task_tier. Abort only a later step that ignores the hint; terminate // alone cannot stop siblings whose calls already returned. if (block && toolName !== "set_task_tier" && toolName !== "ask_user") { const decision = transitionToolDecision(transitionHintDelivered, transitionDiversions, block.message); transitionDiversions = decision.nextCount; transitionBatchRefused = true; if (decision.terminate) ctx?.abort?.(); return { block: true, terminate: decision.terminate, reason: decision.reason }; } if (!["bash", "edit", "write"].includes(toolName)) return; const gate = processPreEffectGate(processState, "write"); if (!gate) return; if (gate.reason !== "action_confirmation_unsupported") return { block: true, reason: gate.message }; const bound = taskTriage?.current; // Pi's tool_call contract provides the exact call id and input; absent fields cannot be bound. if (!bound || bound.taskId !== noProgress.taskId() || !event.toolCallId || !event.input || !ctx) return { block: true, reason: gate.message }; const action = { taskId: bound.taskId, inputRevision: bound.revision, actionId: String(event.toolCallId), operation: String(event.toolName), target: crypto.createHash("sha256").update(JSON.stringify(event.input)).digest("hex"), cwd: ctx.cwd }; const observeAction = (status: TaskTriageActionObservation) => { try { const record = taskTriageActionAuditRecord(action, status); if (record) pi.appendEntry("agent-hub-task-triage-action-observation", record); } catch { /* Diagnostic trace cannot alter effect authorization. */ } }; observeAction("requested"); const grant = await confirmTaskTriageAction(action, { taskId: () => noProgress.taskId(), inputRevision: () => taskTriage?.current?.revision ?? "", ask: (id, question, askCtx, signal) => requestRuntimeAsk(pi.events, id, question, askCtx, signal), startWait: id => executionHistory.startAskUser(id), endWait: (id, sameTask) => { const wait = executionHistory.endAskUser(id, Date.now()); if (sameTask && wait > 0) { taskClock = addTaskClockWait(taskClock, wait); turnBudgetAskUserWaitMs += wait; } }, }, ctx, contract => { pi.appendEntry("agent-hub-task-triage-action-grant", contract); return true; }, undefined, { input: event.input, cwd: ctx.cwd }); if (processBlock() || ctx.cwd !== action.cwd || String(event.toolName) !== action.operation || String(event.toolCallId) !== action.actionId || noProgress.taskId() !== action.taskId || taskTriage?.current?.revision !== action.inputRevision || crypto.createHash("sha256").update(JSON.stringify(event.input)).digest("hex") !== action.target) return { block: true, reason: "Action changed while confirmation was pending; effect refused." }; if (!grant || !consumeActionGrant(grant, action)) { observeAction("not_granted"); return { block: true, reason: gate.message }; } try { pi.appendEntry("agent-hub-task-triage-action-consumed", action); } catch { observeAction("consumption_failed"); return { block: true, reason: "One-use authorization consumption could not be persisted; effect refused." }; } }); pi.on("tool_execution_start", async event => turnHandlers.toolStart(event)); observeAskUserResults(({ params, result, phase }) => { const pack = capabilityConfirmationPack(params.context); if (!pack) return; if (phase === "start") workModePolicy.setCapabilityConfirmation(pack, "pending"); else { const outcome = confirmationOutcome(result); if (!outcome) return; workModePolicy.setCapabilityConfirmation(pack, outcome); resolveIncomingCapabilities(""); applyWorkModeTools(); } }); pi.on("tool_execution_end", async event => turnHandlers.toolEnd(event)); // ── System Prompt Override ─────────────────── const hubPromptCtx: HubPromptContext = { getTriageBeforeDispatch: () => triageRuntime?.orchestratorBeforeDispatch === true, getAgenticAskMode: () => agenticHubRuntime(pi)?.mode ?? "off", getArtifactRoot: () => sessionDir ? artifactsRoot() : null, getCapabilityResolution, getActiveTools: () => pi.getActiveTools(), getToolCatalogNotice: () => [latestToolCatalogDelta ? toolCatalogNotice(latestToolCatalogDelta) : "", latestUnknownToolNotice].filter(Boolean).join("\n"), getAgents: () => Array.from(agentStates.values()).map(state => ({ name: state.def.name, displayName: displayName(state.def.name), description: state.def.description, tools: state.def.tools, })), getResearchPersonas: () => researchPersonas.map(def => ({ name: def.name, displayName: displayName(def.name), description: def.description, model: resolvedModel(def), thinking: resolveThinkingLevel(resolvedThinking(def)), })), getPromptState: () => ({ taskTier: taskTier ?? DEFAULT_TASK_TIER, taskTierAssumed, processRisk: processState.risk, processScope: processState.scope, processOpen: Object.entries(evaluateProcessObligations(processState, { writable: true, budgetTier: taskTier ?? DEFAULT_TASK_TIER }).obligations).filter(([, value]) => value.status === "open").map(([name]) => name), turnDispatchCount, turnResearchCount, taskDispatchCount, taskResearchCount, taskReviewRounds, turnBudget: currentBudget(), taskBudget: currentTaskBudget(), provisionalConfirmations: getCapabilityResolution().provisional .filter(pack => workModePolicy.getCapabilityConfirmation()[pack as ConfirmableCapabilityPack] !== "declined") .map(pack => ({ pack, reason: getCapabilityResolution().reasons[pack], question: capabilityConfirmationQuestion(pack as ConfirmableCapabilityPack), })), }), getWorkMode, getActiveTeamName: () => activeTeamName, getUserLanguage: () => userLanguage, getRulesProtocol: buildRulesProtocol, getDocsProtocol: buildDocsProtocol, isAskUserAvailable: () => askUserAvailable, isComsReady: () => comsReady, getIdentity: () => identity, isHerdrFleetReady: () => herdrFleetReady, }; // This is also called by /af-context before the first turn. Keep prompt assembly // in this one production path so its ledger describes the exact next replacement. function buildHubSystemPrompt(): { systemPrompt: string } { const built = assembleHubPrompt(hubPromptCtx); lastHubLedger = built.ledger; const names = new Set(Array.from(agentStates.values(), s => s.def.name.toLowerCase())); const missingRoles = missingProcessRoleRecoveryHints(processState, names); const additions = activeAdditionRecoveryHints(processState, noProgress.taskId(), taskTriage?.current?.revision); return { systemPrompt: `${built.systemPrompt}\n\nTask-triage (experimental, uncertain): ${JSON.stringify(taskTriage?.status ?? { status: "skipped", detail: "consumer_off" })}. This does not change budget or authorize effects.${processBlock() ? ` BLOCKED: ${processBlock()!.message}` : ""}${missingRoles ? ` ${missingRoles} Never auto-raise tier or substitute an unrelated role.` : ""}${additions ? ` ${additions}` : ""}` }; } const turnHandlers = createTurnLifecycleHandlers({ setTurnState: state => coms.setTurnState(state), finishMonitorTurn: () => { if (monitorBridge && monitorTurnId) { monitorBridge.finishParent(monitorTurnId, "completed"); monitorTurnId = null; } }, startMonitorTurn: () => { if (monitorBridge && monitorHubId) { monitorTurnId = `hub-turn-${monitorHubId}-${crypto.randomUUID()}`; monitorBridge.startParent({ id: monitorTurnId, hubInstanceId: monitorHubId, checkoutId: currentCtx?.cwd || process.cwd() }); } }, startAskUser: id => executionHistory.startAskUser(id), endAskUser: (id, at) => executionHistory.endAskUser(id, at), acknowledgeExternalBlocker: () => { externalBlockerAcknowledged = true; externalBlockerRefusedOnce = false; }, addAskUserWait: waitMs => { if (waitMs > 0) taskClock = addTaskClockWait(taskClock, waitMs); turnBudgetAskUserWaitMs += waitMs; }, applyWorkMode: applyWorkModeTools, closeTurnActiveTime, openTaskClock: at => { taskClock = openTaskClock(taskClock, at); }, startHistoryTurn: at => executionHistory.startTurn(at), resetTurnBudgetState: () => { turnBudgetAskUserWaitMs = 0; // budgetRecovery is task-scoped: prose/new turns cannot clear a declined confirmation. turnDispatchCount = 0; turnResearchCount = 0; turnDispatchFingerprints.clear(); externalBlockerAcknowledged = true; externalBlockerRefusedOnce = false; if (turnReport.dispatches.length > 0 || turnReport.research > 0 || turnReport.refusals > 0) { lastTurnReport = turnReport; sessionTotals.turns++; } turnReport = freshTurnReport(); }, updateModeStatus, buildPrompt: buildHubSystemPrompt, endHistoryTurn: at => executionHistory.endTurn(at), unaddressedPeerWarning: () => unaddressedPeerSweep(Array.from(hubSpawnedPeers.values()))?.message ?? null, respondToPeer: ctx => coms.respond(ctx), }); pi.on("before_agent_start", async () => { if (taskTriageTestTransportActive && process.env.AGENT_HUB_TASK_TRIAGE_FAKE_STATE_RECORD) { try { fs.appendFileSync(process.env.AGENT_HUB_TASK_TRIAGE_FAKE_STATE_RECORD, JSON.stringify({ pid: process.pid, phase: "before_agent_start", consumerPresent: !!taskTriage, consumerStatus: taskTriage?.status.status ?? null, consumerDetail: taskTriage?.status.detail ?? null, calls: taskTriage?.calls ?? null, processPersistenceBlocked, rosterBlocked: !!currentCtx && modelWorkBlockedByRosterRecovery(currentCtx), pendingTaskTransition }) + "\n"); } catch { /* diagnostic only */ } } // Pressure/roster recovery gates run first; assessment runs before this model input. if (taskTriage && !processPersistenceBlocked && (!currentCtx || !modelWorkBlockedByRosterRecovery(currentCtx))) { // Persist the first task identity before binding a pre-model assessment: // compaction/resume must not mint a new ID for the same cached input. try { noProgress.persistTaskIdentity(); } catch { processPersistenceBlocked = true; } const assessment = processPersistenceBlocked ? null : await taskTriage.evaluate(noProgress.taskId()); // Guarded offline fixture diagnostics: metadata only, never task text or provider payload. if (taskTriageTestTransportActive && process.env.AGENT_HUB_TASK_TRIAGE_FAKE_STATE_RECORD) { try { fs.appendFileSync(process.env.AGENT_HUB_TASK_TRIAGE_FAKE_STATE_RECORD, JSON.stringify({ pid: process.pid, phase: "assessment", inputRevision: assessment?.revision ?? null, fingerprint: assessment?.fingerprint ?? null, taskId: assessment?.taskId ?? null, status: assessment?.assessment.status ?? null, calls: taskTriage.calls, pendingTaskTransition, processPersistenceBlocked }) + "\n"); } catch { /* diagnostic only */ } } if (assessment && !pendingTaskTransition && !processPersistenceBlocked) { try { const next = applyTaskTriageAdditions(processState, assessment.assessment, { taskId: assessment.taskId, evaluationId: assessment.evaluationId, inputRevision: assessment.revision }); pi.appendEntry("agent-hub-process-state", processAuditRecord(next, evaluateProcessObligations(next, { writable: true, budgetTier: taskTier ?? DEFAULT_TASK_TIER }))); processState = next; } catch { processPersistenceBlocked = true; } } } // Assessment can open a plan/review lease after input resolved its original packs. if (taskTriage) workModePolicy.refreshCapabilities(); const result = turnHandlers.beforeAgentStart(); toolCatalogRuntime.beginTurn(); return result; }); const pressureLifecycle = createContextPressureLifecycle({ getState: () => pressureRootState, setPressure: value => { contextPressureState = value; pressureRootState.pressure = value; }, getCurrentContext: () => currentCtx, appendEntry: (type, data) => pi.appendEntry(type, data), sendUserMessage: (content, options) => pi.sendUserMessage(content as any, options), resolveCapabilities: resolveIncomingCapabilities, applyWorkMode: applyWorkModeTools, modelWorkBlocked: modelWorkBlockedByRosterRecovery, onAcceptedInput: (text, source, replayed) => { if ((source === "extension" && !replayed) || !text.trim()) return; if (evaluatedTaskTriageInputChanged(taskTriage?.current ?? null, text)) { pendingTaskTransition = true; transitionDiversions = 0; transitionHintDelivered = false; transitionBatchRefused = false; try { pi.appendEntry(TASK_TRANSITION_ENTRY_TYPE, { pending: true }); } catch { processPersistenceBlocked = true; } } hubTaskText = text; taskTriage?.input(text, "interactive"); }, }); function replayDeferredRecoveryInputs(): void { pressureLifecycle.replayDeferred(); } pi.on("message_end", async (event, ctx) => { const diagnostics = observeUnknownToolCalls({ message: event.message, catalog: toolCatalogRuntime.catalogForMessage(), taskId: unknownToolCounter.snapshot().activeTaskId || noProgress.taskId(), counter: unknownToolCounter, seenCallIds: seenUnknownToolCalls }); for (const diagnostic of diagnostics) { latestUnknownToolNotice = unknownToolNotice(diagnostic); try { pi.appendEntry(UNKNOWN_TOOL_COUNTER_ENTRY_TYPE, { snapshot: unknownToolCounter.snapshot(), diagnostic }); } catch {} } pressureLifecycle.messageEnd(event, ctx); }); pi.on("turn_start", event => { orchestratorSelfReadUsed = 0; hubCapture.start(event.turnIndex); }); pi.on("turn_end", async (event, ctx) => { if (transitionBatchRefused && pendingTaskTransition) transitionHintDelivered = true; transitionBatchRefused = false; pressureLifecycle.turnEnd(ctx); const text = event.message.role === "assistant" ? event.message.content.filter(c => c.type === "text").map(c => c.text).join("") : ""; hubCapture.end(event.turnIndex, text); }); pi.on("context", async (event, ctx) => { pressureLifecycle.context(ctx); const context = hubCapture.hubContext(); const text = context && proactiveRuntime?.feedback?.take("hub", "direct", context); if (text) { proactiveHubDeliveries++; return { messages: [...event.messages, { role: "custom" as const, customType: "agent-fleet.proactive-advisory", content: text, display: false, timestamp: Date.now() }] }; } }); pi.on("agent_settled", async (_event, ctx) => pressureLifecycle.agentSettled(ctx)); pi.on("session_compact", async (_event, ctx) => { noProgress.compact(ctx.sessionManager.getEntries()); if (!processPersistenceBlocked) persistProcessVerdict(processState, evaluateProcessObligations(processState, { writable: true, budgetTier: taskTier ?? DEFAULT_TASK_TIER })); const result = toolCatalogRuntime.compact({ mode: getWorkMode(), getEffectiveTools: () => pi.getActiveTools(), persist: (type, data) => pi.appendEntry(type, data), counterSnapshot: () => unknownToolCounter.snapshot(), retainCounter: () => unknownToolCounter.noteCompaction(), establishToolStateChange: (previous, next, evidenceRef) => noProgress.establishToolStateChange(previous, next, evidenceRef), settle: () => pressureLifecycle.sessionCompact(), onError: error => { try { pi.appendEntry("agent-hub-tool-catalog-error", { reason: "compaction_restore", error: error instanceof Error ? error.message : String(error) }); } catch {} }, }); if (result) latestToolCatalogDelta = result.delta; }); pi.on("input", async (event, ctx) => { const outcome = pressureLifecycle.input(event, ctx); return { action: outcome.action }; }); // ── Session Start ──────────────────────────── let sessionOverrides: ReturnType | undefined; let sessionSoloMode = false; const monitorSession = createMonitorSession({ pi, getBridge: () => monitorBridge, setBridge: value => { monitorBridge = value; }, getLifecycle: () => monitorLifecycle, setLifecycle: value => { monitorLifecycle = value; }, setHubId: value => { monitorHubId = value; }, getOwnerId: () => monitorOwnerId, setOwnerId: value => { monitorOwnerId = value; }, queueDepth: () => inboundQueue.size, }); registerSessionOrchestration(pi, { resetSession: (_ctx) => resetHubSession(_ctx, { registerVersion: registerVersionStatus, resetPressure: pressureLifecycle.reset, clearRosterRecovery: workModePolicy.clearRosterRecovery, captureBaselineTools: () => { baselineTools = pi.getActiveTools(); }, resetAccessApproval: accessApprovalRouter.reset, terminateResearch: () => { for (const st of researchStates.values()) if (st.proc && st.status === "running") { st.killedByOperator = true; st.proc.kill("SIGTERM"); } }, resetResearch: researchRuntime.reset, resetHistory: executionHistory.reset, resetBudgets: () => { hubCapture.reset(); hubTaskText = undefined; proactiveRuntime = null; proactiveConfig = null; system1Snapshot = normalizeSystem1Config(null); resetAgenticHub(pi); resetDiscoveryHub(pi); proactiveHubDeliveries = 0; taskClock = createTaskClock(); turnBudgetAskUserWaitMs = 0; turnContinuationCount = 0; taskContinuationCount = 0; budgetRecovery.reset(); noProgress.prepareSessionRestore(); resetUnknownToolCounterForCurrentTask(); toolCatalogRuntime.restore(catalogSnapshot(getWorkMode(), [])); latestToolCatalogDelta = null; }, clearWidgets: _ctx => { fleetUiGeneration++; fleetActions?.reset(); gridUI.dispose(); }, closeDelegationWatchers: () => { for (const st of agentStates.values()) { st.delegationsWatcher?.close(); st.delegationsWatcher = undefined; } }, resetSessionState: ctx => { delegatedTokens = 0; hubSpawnedPeers.clear(); widgetCtx = ctx; contextWindow = ctx.model?.contextWindow || 0; gridUI.reset(); }, resolveSafety: cwd => Boolean(safetyHarnessPath = resolveSafetyHarness(cwd)), resolveDelegate: cwd => { delegateExtPath = resolveDelegateExtension(cwd); }, }), restartMonitor: _ctx => monitorSession.restart(_ctx), initializeComs: async (_ctx) => { // ── Embedded coms init ── // Always refresh the ctx the coms handlers use. Bind the endpoint + register // in the pool exactly once per process (guard on comsReady), so a /new session // keeps the same peer identity rather than leaking a second socket. On any // failure we degrade: comsReady stays false and the coms_* tools are withheld. hubStateCtx.setCurrentContext(_ctx); sessionSoloMode = pi.getFlag("solo") === true; if (!comsReady && !sessionSoloMode) { try { identity = await coms.connect({ ctx: _ctx, defaultNamePrefix: "hub", defaultPurpose: "agent-hub dispatcher" }); comsReady = true; try { _ctx.ui.setStatus("coms", `📡 ${identity.name}@${identity.project}`); } catch { /* hasUI may be false — non-fatal */ } } catch (err) { comsReady = false; try { _ctx.ui?.notify?.(`📡 coms: init failed — ${err instanceof Error ? err.message : String(err)} (coms tools disabled)`, "error"); } catch { /* ignore */ } } } }, initializeExemptions: (_ctx) => { try { system1Snapshot = loadSystem1Snapshot(_ctx.cwd || process.cwd()); const config = loadProactiveConfig(_ctx.cwd || process.cwd(), system1Snapshot); if (isCaptureEnabled(config)) proactiveConfig = config; } catch { /* invalid config fails closed */ } // ── Damage-control shared exemptions file ── // One per hub session (solo mode included). Exporting the path on our own // process.env lets the co-loaded damage-control-continue mirror /af-allow // session grants into the same file the spawned children read. if (!exemptionsFile) { hubStateCtx.setExemptionsFile(exemptionsFilePath(identity?.session_id ?? `hub-solo-${process.pid}`)); process.env[EXEMPTIONS_FILE_ENV] = exemptionsFile!; } }, loadAgents: (_ctx) => { // Allocate a fresh namespace; shared legacy files belong to prior/live sessions. sessionOverrides = parseAgentTeamOverrides(_ctx.cwd); try { const config = loadProactiveConfig(_ctx.cwd,system1Snapshot,sessionOverrides.policyRoots); proactiveConfig = isCaptureEnabled(config) ? config : null; } catch { proactiveConfig = null; /* invalid consent fails closed */ } runHistoryKeep = sessionOverrides.runHistoryKeep; loadAgents(_ctx.cwd); // Composition follows applyOverrides: approved roots and the shared service do not exist yet. // Surface non-fatal persona frontmatter warnings (skipped subagents roles, // bad delegate_depth) once per session. const fmWarnings = allAgentDefs.flatMap(d => (d.warnings || []).map(w => `${d.name}: ${w}`)); if (fmWarnings.length > 0) { _ctx.ui.notify(`Persona frontmatter warnings:\n${fmWarnings.join("\n")}`, "warning"); } if (!delegateExtPath && allAgentDefs.some(d => d.subagents)) { _ctx.ui.notify( "delegate.ts not found next to agent-hub — `subagents:` declarations are inert (specialists dispatch without a delegate tool).", "warning", ); } }, applyOverrides: (_ctx) => { if (!sessionOverrides) throw new Error("session_start applyOverrides ran before loadAgents"); watchdogSystem1 = disposeWatchdogSystem1Session(watchdogSystem1); communicationStore.dispose(); triageRuntime?.dispose(); triageRuntime = null; taskTriage?.dispose(); taskTriage = null; taskTriageActivity?.dispose(); taskTriageActivity = null; try { watchdogActivity?.dispose(); } catch { /* trace disposal must not block the session */ } // Stable for a real session across snapshot replacement; G2 must not split one session into artificial samples. watchdogActivity = sessionDir ? createWatchdogActivity({ directory: `${sessionDir}/artifacts/watchdog`, sessionId: path.basename(sessionDir) }) : null; const taskTriageTestTransport = resolveTaskTriageTestTransport(); taskTriageTestTransportActive = !!taskTriageTestTransport; watchdogSystem1 = createWatchdogSystem1Session({ ...readWatchdogSystem1Snapshot({ cwd: _ctx.cwd, snapshot: system1Snapshot, configuredMode: sessionOverrides.watchdogSystem1Mode, watchdogSetting: sessionOverrides.watchdogSetting, env: process.env, transport: taskTriageTestTransport, warnings: sessionOverrides.warnings, }), wrapService: service => communicationStore.wrap(service, { provider: "typesafe", model: "jev-1.13.0" }) }); const triageConfig = (system1Snapshot?.consumers.dispatchTriage.config ?? null) as TriageConfig | null; const taskTriageConfig = loadTaskTriageConfig(_ctx.cwd || process.cwd(), system1Snapshot); const sharedService = watchdogSystem1?.sharedService; configureAgenticHub(pi, { snapshot: system1Snapshot, service: sharedService, ctx: _ctx, sessionDir, communicationStore, taskId: () => noProgress.taskId() }); configureDiscoveryHub(pi, { snapshot: system1Snapshot, service: sharedService, ctx: _ctx, sessionDir, communicationStore, taskId: () => noProgress.taskId() }); const serviceUnavailableReason = watchdogSystem1?.readiness.status === "ready" ? "unavailable" : watchdogSystem1?.readiness.reason ?? "unavailable"; taskTriageConfigured = taskTriageConfig.status !== "missing"; taskTriageConfigStatus = taskTriageConfig.status === "active" ? "active" : taskTriageConfig.status === "invalid" ? "invalid" : "off"; taskTriageActivity = taskTriageConfig.status === "active" ? createTaskTriageActivity({ directory: sessionDir ? path.join(sessionDir, "artifacts/task-triage-activity") : undefined }) : null; taskTriage = taskTriageConfig.status === "active" ? createTaskTriageRuntime({ root: _ctx.cwd || process.cwd(), service: sharedService, serviceUnavailableReason, persist: state => pi.appendEntry(TASK_TRIAGE_RUNTIME_ENTRY, state), observer: taskTriageActivity! }) : null; if (taskTriageTestTransportActive && process.env.AGENT_HUB_TASK_TRIAGE_FAKE_STATE_RECORD) { try { fs.appendFileSync(process.env.AGENT_HUB_TASK_TRIAGE_FAKE_STATE_RECORD, JSON.stringify({ pid: process.pid, phase: "service_init", configStatus: taskTriageConfig.status, consumerPresent: !!taskTriage, fakeTransportActive: taskTriageTestTransportActive, sharedServiceReady: !!sharedService, serviceUnavailableReason }) + "\n"); } catch { /* diagnostic only */ } } if (taskTriage && !sharedService) { try { _ctx.ui.notify(`Task triage is enabled but unavailable (${serviceUnavailableReason}); no inference or new requirements will be added. Existing process obligations remain enforced.`, "warning"); } catch { /* Optional UI must not change process enforcement. The prompt retains the status. */ } } triageRuntime = createTriageRuntime({ config: triageConfig, service: watchdogSystem1?.sharedService, current: input => triageInput(input.task,input.scope,input.language,input.domain), trace: event => pi.appendEntry("agent-hub-triage", event), }); applySessionOverrides(_ctx, sessionOverrides, { setLanguage: value => { userLanguage = value; }, setReconTimeout: value => { reconSearchTimeoutMs = value; }, setBudgetOverrides: value => { budgetOverrides = value; }, setWatchdog: (setting, judge) => { watchdogSetting = setting; watchdogJudgeModel = judge; }, resetTurnCounts: () => { turnDispatchCount = 0; turnResearchCount = 0; }, // Session setup clears in-memory budgets before restore, not the saved task. // A persisted reset here would append a new task event after the old one. resetTaskWindow: () => resetTaskWindow(null, Date.now(), false), updateModeStatus, setProjectRules: value => { projectRulesDirs = value; }, setProjectDocs: value => { projectDocsPaths = value; }, resetModelPolicy: () => {modelPolicy.reset();profileActivation.reset();applyWorkModeTools();}, getModelProfileErrors: () => modelProfileErrors, getAgentDefs: () => allAgentDefs, getModelProfiles: () => modelProfiles, deleteModelProfile: name => { delete modelProfiles[name]; }, allowedModels, getDispatchPolicyWarnings: () => dispatchPolicyWarnings, setResearchPersonas: value => { researchPersonas = value; }, }); if (proactiveConfig && sessionDir) { try { proactiveRuntime = composeHubProactive({ config: proactiveConfig, root: _ctx.cwd, sessionDir, rulesRoots: sessionOverrides.rulesDirs, policyRoots: sessionOverrides.policyRoots, service: watchdogSystem1?.sharedService, capture: hubCapture, onChange: updateWidget }); } catch { proactiveRuntime = null; proactiveConfig = null; } // unavailable composition never reports reviewed } }, restoreRoster: (_ctx) => { // Explicit CLI selection wins; otherwise restore only the canonical team name // and re-resolve it against current teams.yaml/persona files. An explicit // Operator work mode suppresses an ambient persisted roster unless --agent-team // was also supplied. agentStates.clear(); activeTeamName = ""; comsMissNotified.clear(); recomputeGrid(); const sessionEntries = _ctx.sessionManager.getEntries(); // Resume restores the persisted fence. A missing entry is the legacy clear state. pendingTaskTransition = latestPendingTaskTransition(sessionEntries); transitionDiversions = 0; transitionHintDelivered = false; transitionBatchRefused = false; try { processState = latestProcessState(sessionEntries); processPersistenceBlocked = false; } catch { processState = createProcessState(); processPersistenceBlocked = true; } taskTriage?.restore(sessionEntries); const savedTaskIds = new Set(processState.additions?.map(addition => addition.taskId)); noProgress.restore(sessionEntries, savedTaskIds.size === 1 ? [...savedTaskIds][0] : undefined); if (processTaskIdentityConflicts(processState, noProgress.taskId())) processPersistenceBlocked = true; if (taskTriageTestTransportActive && process.env.AGENT_HUB_TASK_TRIAGE_FAKE_STATE_RECORD) { try { fs.appendFileSync(process.env.AGENT_HUB_TASK_TRIAGE_FAKE_STATE_RECORD, JSON.stringify({ pid: process.pid, phase: "restore", processPersistenceBlocked, savedTaskIdCount: savedTaskIds.size, taskIdMatchesAddition: savedTaskIds.size === 1 && savedTaskIds.has(noProgress.taskId()), consumerPresent: !!taskTriage, calls: taskTriage?.calls ?? null, consumerStatus: taskTriage?.status.status ?? null }) + "\n"); } catch { /* diagnostic only */ } } const persistedCatalog = latestPersistedToolCatalog(sessionEntries); if (persistedCatalog) toolCatalogRuntime.restore(persistedCatalog); const persistedUnknownTools = latestPersistedUnknownToolCounter(sessionEntries); if (persistedUnknownTools) unknownToolCounter = restoreUnknownToolCounter(persistedUnknownTools, { limit: 3 }); const explicitWorkMode = pi.getFlag("work-mode"); const explicitRoster = pi.getFlag("agent-team"); const hasExplicitRoster = typeof explicitRoster === "string" && explicitRoster.trim() !== ""; const startupRoster = resolveSessionRoster({ teams, entries: sessionEntries, explicitRoster, availablePersonas: allAgentDefs.map(def => def.name), includePersisted: !(explicitWorkMode === "operator" && !hasExplicitRoster), }); workModePolicy.setRestoredWorkMode(resolveSessionWorkMode({ entries: sessionEntries, explicitWorkMode, hasExplicitRoster: startupRoster.source === "explicit", })); if (startupRoster.roster) { activateTeam(startupRoster.roster.name); persistActiveRoster(); } const rosterRecoveryRequired = orchestratorNeedsRoster(getWorkMode(), agentStates.size); const rosterRecoveryDiagnostic = rosterRecoveryRequired ? startupRoster.diagnostic || "Persisted orchestrator work mode has no native roster." : ""; workModePolicy.setRosterRecovery(rosterRecoveryRequired, rosterRecoveryDiagnostic); if (startupRoster.diagnostic) { _ctx.ui.notify( `${startupRoster.diagnostic} ${rosterRecoveryRequired ? "Model input is blocked until you select /af-agents-team, restart with --agent-team , or switch explicitly with --work-mode operator." : "Continuing without that roster."}`, rosterRecoveryRequired ? "error" : "warning", ); } else if (rosterRecoveryRequired) { _ctx.ui.notify( `${rosterRecoveryDiagnostic} Model input is blocked until you select /af-agents-team, restart with --agent-team , or switch explicitly with --work-mode operator.`, "error", ); } }, resolveCapabilities: async (_ctx) => { // Probe for `ask_user` (registered by the `pi-ask-user` companion package // when installed). Action methods like getAllTools are runtime-only, so // this MUST happen at session_start, not at extension load. askUserAvailable = pi.getAllTools().some(t => t.name === "ask_user"); // Fleet tools are available only inside a herdr pane with a live server. // The work mode policy adds gated groups without activating unavailable tools. herdrFleetReady = herdrPaneId() !== null && (await herdrAvailable()) !== null; const persistedCapabilities = latestPersistedCapabilityState(_ctx.sessionManager.getEntries()); workModePolicy.restoreCapabilities({ taskPacks: persistedCapabilities?.taskPacks ?? [], provisional: persistedCapabilities?.provisional ?? [], confirmation: persistedCapabilities?.confirmation ?? {} }); resolveIncomingCapabilities(""); applyWorkModeTools(); if (pressureLifecycle.observe(_ctx, "session_start") === "compact-now") { setTimeout(() => pressureLifecycle.runCompaction(_ctx, "session_start"), 0); } workModePolicy.updateStatus(_ctx); }, notifyStartup: (_ctx) => { _ctx.ui.setStatus("agent-team", `Native roster: ${activeTeamName || "(none)"} (${agentStates.size})`); const members = Array.from(agentStates.values()).map(s => displayName(s.def.name)).join(", "); const askUserLabel = askUserAvailable ? "available (via pi-ask-user)" : "NOT AVAILABLE — run `pi install npm:pi-ask-user`"; const comsLabel = comsReady && identity ? `📡 ${identity.name}@${identity.project} — peers via coms_list; /af-handoff to delegate` : sessionSoloMode ? "off (--solo: fixed specialists + research only)" : "off (endpoint bind failed — coms tools disabled)"; const fleetLabel = herdrFleetReady ? "herdr — spawn/read/close panes + notify (herdr_* tools active)" : "off (not inside a herdr pane, or no herdr server)"; const comsPreferred = Object.entries(dispatchPolicy.substitutions) .filter(([, s]) => s.prefer === "coms") .map(([n]) => n); const dispatchLabel = dispatchPolicy.default === "coms" ? "coms default — any member with a live same-name pool peer is served by it (/af-dispatch-policy)" : comsPreferred.length > 0 ? `coms-preferred: ${comsPreferred.join(", ")} (live peer wins, /af-dispatch-policy for status)` : "all native (no substitutions in .pi/agents/dispatch-policy.yaml)"; _ctx.ui.notify(buildSessionStartNotice({ workMode: getWorkMode(), activeTeamName, agentCount: agentStates.size, members, dispatchLabel, userLanguage, askUserLabel, comsLabel, fleetLabel, }), "info"); }, updateWidget: (_ctx) => { updateWidget(); }, installFooter: (_ctx) => { _ctx.ui.setFooter(createSessionFooter({ ctx: _ctx, version: HARNESS_VERSION ?? "unknown", getModel: () => _ctx.model?.id || "no-model", getThinkingLevel: () => pi.getThinkingLevel?.() ?? undefined, thinkingSuffix, getHint: () => composeFleetFooterHint(compactWorkMode(getWorkMode())), renderLeft: renderHubFooterLeft, truncateToWidth, visibleWidth, })); }, }, { finishEvidence: () => { if (sessionDir) closeEvidenceSession(sessionDir); }, shutdownComs: () => { unregisterQuestions(); return coms.shutdown(); }, shutdownMonitor: () => monitorSession.shutdown(), removeExemptions: () => { if (!exemptionsFile) return; try { fs.unlinkSync(exemptionsFile); } catch {} hubStateCtx.setExemptionsFile(null); }, terminateChildren: () => { hubCapture.reset(); proactiveRuntime = null; watchdogSystem1 = disposeWatchdogSystem1Session(watchdogSystem1); communicationStore.dispose(); triageRuntime?.dispose(); triageRuntime = null; taskTriage?.dispose(); taskTriage = null; taskTriageActivity?.dispose(); taskTriageActivity = null; taskTriageConfigStatus = "off"; taskTriageConfigured = false; try { watchdogActivity?.dispose(); } catch { /* trace disposal must not block shutdown */ } watchdogActivity = null; for (const st of [...agentStates.values(), ...researchStates.values()]) { fenceOperatorCancel(st); if (st.proc && st.status === "running") try { st.killedByOperator = true; st.proc.kill("SIGTERM"); } catch {} } }, clearPoolWidget: () => { gridUI.dispose(); if (currentCtx?.hasUI) try { currentCtx.ui.setWidget("coms-pool", undefined); } catch {} }, }); // Ordered end-turn peer response remains after session-start registration. pi.on("agent_end", async (_event, ctx) => { toolCatalogRuntime.endTurn(); await turnHandlers.agentEnd(ctx); }); }