import { spawn, spawnSync } from "node:child_process"; import { createHash, randomUUID } from "node:crypto"; import * as fs from "node:fs"; import * as os from "node:os"; import * as path from "node:path"; import { fileURLToPath } from "node:url"; import { createGzip, gunzipSync } from "node:zlib"; import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; import { parseFrontmatter, VERSION as PI_VERSION } from "@earendil-works/pi-coding-agent"; import { assertRuntimeBaseline, registerRuntimeBaseline } from "./runtime-baseline.ts"; import { mergeRoute, mergeSandbox } from "../../scripts/runtime/config-observation.mjs"; import { Type } from "typebox"; import { foldReviewHistory, normalizeReviewPlan, normalizeReviewVerdict, reviewAgentMatches, validateReviewIdentity } from "./review-budget.ts"; import { validateCloseoutRequest } from "../../scripts/runtime/closeout-recovery.mjs"; import { EXECUTION_PROTOCOL_DIGEST, assertExecutionSupport, spawnObservedExecution, inspectExecution } from "../../scripts/runtime/review-execution.mjs"; import { beginRecoverableHostCloseout, beginHostCloseoutDispatch, completeStructuredReviewOutcome, deriveReviewPhysicalAttempt, recordReviewAbortHold, recordReviewCompletion, recordReviewHold, recordReviewPhysicalOutcome, reconcileReviewExecution, reserveReviewStart, reserveReviewStartAsync, resolvePlanReviewAuthority } from "../../modules/pidex/analysis-metrics-history/lib/review-lifecycle.mjs"; import { resolveStateRoot } from "../../modules/pidex/analysis-metrics-history/lib/state-root.mjs"; import { bootstrapRuleInventoryProjections, lifecycleRulePhase, openRuleLifecycleStore, prepareLifecycleRuntimeContext } from "../../scripts/quality/rule-lifecycle-store.mjs"; import { renderVerifiedRuntimeRules, validateRequiredReviewerProducer } from "../../scripts/quality/rule-mirror-sync.mjs"; import { createHostAutomaticLearningRunner, normalizeAutomaticLearningLsRemoteOutput, openAutomaticLearningRuntimeSource, runAutomaticRuleLearningCoordinatorAsync } from "../../scripts/quality/rule-lifecycle.mjs"; import { closedLifecycleActionHistoryAdapter, invokeLifecycleActionFromOrdinaryResult } from "../../modules/pidex/project-pipeline/lib/rule-lifecycle-action.mjs"; type AgentFrontmatter = { name?: string; description?: string; model?: string; tools?: string; maxTurns?: string | number; }; type JsonEvent = Record; type RpResult = { agent: string; provider?: string; model?: string; modelRequested?: string; effort?: string; exitCode: number; stderr: string; finalText: string; fallbackFrom?: string; logFile?: string; runDir?: string; sessionDir?: string; stdoutEventCount?: number; skippedLargeEventCount?: number; toolCount?: number; turnCount?: number; maxTurns?: number; durationMs?: number; inputChars?: number; outputChars?: number; inputTokensEstimate?: number; outputTokensEstimate?: number; cacheReadTokens?: number; cacheWriteTokens?: number; costUsdEstimate?: number; metricsFile?: string; routeSource?: string; setupError?: boolean; timedOut?: boolean; aborted?: boolean; turnLimitHit?: boolean; warnings?: string[]; reviewCompletion?: { status: string; holdId?: string }; }; type ParallelSecondaryRoute = { provider?: string; model?: string; effort?: string; label?: string; timeout_seconds?: number; }; type EligibleParallelLane = { lane_id?: string; agent?: string; trigger?: string; runner_provider?: string; runner_model?: string; effort?: string; timeout_seconds?: number; }; type HostAgentRoute = AgentRoute & { provider: string; timeoutSeconds?: number; routeSource: string; }; type AgentRoute = { provider?: string; model?: string; effort?: string; timeout_seconds?: number; notes?: string; condition?: string; parallel_secondary?: ParallelSecondaryRoute[]; permission_mode?: "default" | "auto" | "acceptEdits" | "dontAsk" | "plan" | "bypassPermissions"; tools?: string[]; allowed_tools?: string[]; disallowed_tools?: string[]; add_dirs?: string[]; dangerously_skip_permissions?: boolean; }; type RoutingConfig = { defaults?: AgentRoute; agents?: Record; fallback?: { on_error?: string; retries?: number; reason_prefix?: string; }; }; type SandboxProfile = { enabled?: boolean; image?: string; network_default?: "none" | "default"; memory?: string; cpus?: number; pids_limit?: number; timeout_seconds?: number; preserve_on_failure?: boolean; container_user_mode?: "image-default" | string; container_user_enforced?: boolean; }; type SandboxConfig = { enabled?: boolean; default_mode?: "off" | "hardened-pipeline" | string; profiles?: Record; }; type SandboxState = { mode: "off" | "hardened-pipeline"; enabled: boolean; reason: string; configPath: string; localConfigPath: string; profile?: SandboxProfile; }; type SandboxRuntimeContext = { mode: "hardened-pipeline"; runId: string; hostProjectRoot: string; sandboxWorkspace: string; allowedWriteRoot: string; }; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const BOOTSTRAP_ROOT = path.resolve(__dirname, "../.."); const CANONICAL_HOME_ROOT = process.env.PIDEX_HOME_ROOT ?? path.join(os.homedir(), "pidex"); function readPackageVersion(root: string): string | undefined { try { const pkg = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8")); return typeof pkg?.version === "string" ? pkg.version : undefined; } catch { return undefined; } } const BOOTSTRAP_PACKAGE_VERSION = readPackageVersion(BOOTSTRAP_ROOT); function isPidexRuntimeRoot(root: string): boolean { try { const pkgPath = path.join(root, "package.json"); if (!fs.existsSync(pkgPath)) return false; const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8")); if (pkg?.name !== "@d-trattner/pidex" && pkg?.name !== "pidex") return false; return fs.existsSync(path.join(root, "agents")) && fs.existsSync(path.join(root, "config", "agents.json")) && fs.existsSync(path.join(root, "scripts")); } catch { return false; } } function resolvePidexRuntimeRoot(): string { if (process.env.PIDEX_ROOT && isPidexRuntimeRoot(process.env.PIDEX_ROOT)) return path.resolve(process.env.PIDEX_ROOT); if (isPidexRuntimeRoot(CANONICAL_HOME_ROOT)) return path.resolve(CANONICAL_HOME_ROOT); return BOOTSTRAP_ROOT; } const PACKAGE_ROOT = resolvePidexRuntimeRoot(); const AGENTS_DIR = path.join(PACKAGE_ROOT, "agents"); const CONFIG_PATH = process.env.PIDEX_CONFIG_FILE ?? path.join(PACKAGE_ROOT, "config", "agents.json"); const SANDBOX_CONFIG_PATH = process.env.PIDEX_SANDBOX_CONFIG_FILE ?? path.join(PACKAGE_ROOT, "config", "sandbox.json"); const SANDBOX_LOCAL_CONFIG_PATH = process.env.PIDEX_SANDBOX_LOCAL_CONFIG_FILE ?? path.join(PACKAGE_ROOT, "config", "sandbox.local.json"); const DELEGATE_DIR = path.join(PACKAGE_ROOT, "scripts", "delegate"); const SKILL_PATH = path.join(PACKAGE_ROOT, "skills", "pd", "SKILL.md"); // Plan 059 Slice 4: single supported PIDEX state-root override resolved through the // shared helper (PIDEX_STATE_DIR > RUNNING_PI_STATE_DIR > /state) so host // lifecycle, CLI, event pipeline, and the project TBR serialization lock agree. const STATE_DIR = resolveStateRoot({ root: PACKAGE_ROOT }); const RUNS_DIR = path.join(STATE_DIR, "runs"); const METRICS_DIR = path.join(STATE_DIR, "metrics"); const PRICING_PATH = path.join(PACKAGE_ROOT, "config", "pricing.json"); const PROVIDER_LIMITS_LATEST_PATH = path.join(STATE_DIR, "provider-limits", "latest.json"); const CHECK_AUTH_SCRIPT = path.join(DELEGATE_DIR, "check-auth.sh"); const PROJECT_PIPELINE_MODE_SCRIPT = process.env.PIDEX_PROJECT_PIPELINE_MODE_SCRIPT ?? path.join(PACKAGE_ROOT, "modules", "pidex", "project-pipeline", "scripts", "project-pipeline", "mode-resolver.mjs"); const PROJECT_PIPELINE_RUN_FLOW_SCRIPT = process.env.PIDEX_PROJECT_PIPELINE_RUN_FLOW_SCRIPT ?? path.join(PACKAGE_ROOT, "modules", "pidex", "project-pipeline", "scripts", "project-pipeline", "run-flow.mjs"); const PROJECT_PIPELINE_ORCHESTRATOR_SCRIPT = process.env.PIDEX_PROJECT_PIPELINE_ORCHESTRATOR_SCRIPT ?? path.join(PACKAGE_ROOT, "modules", "pidex", "project-pipeline", "scripts", "project-pipeline", "orchestrator.mjs"); const PROJECT_PIPELINE_STATUS_SCRIPT = process.env.PIDEX_PROJECT_PIPELINE_STATUS_SCRIPT ?? path.join(PACKAGE_ROOT, "modules", "pidex", "project-pipeline", "scripts", "project-pipeline", "status.mjs"); const PROJECT_PIPELINE_RUN_AGENT_SCRIPT = process.env.PIDEX_PROJECT_PIPELINE_RUN_AGENT_SCRIPT ?? path.join(PACKAGE_ROOT, "modules", "pidex", "project-pipeline", "scripts", "project-pipeline", "run-agent.mjs"); const PROJECT_PIPELINE_LIFECYCLE_SCRIPT = process.env.PIDEX_PROJECT_PIPELINE_LIFECYCLE_SCRIPT ?? path.join(PACKAGE_ROOT, "modules", "pidex", "project-pipeline", "scripts", "project-pipeline", "lifecycle.mjs"); const PROJECT_PIPELINE_CREDENTIALS_SCRIPT = process.env.PIDEX_PROJECT_PIPELINE_CREDENTIALS_SCRIPT ?? path.join(PACKAGE_ROOT, "modules", "pidex", "project-pipeline", "scripts", "project-pipeline", "credentials.mjs"); const PROJECT_PIPELINE_PREVIEW_SCRIPT = process.env.PIDEX_PROJECT_PIPELINE_PREVIEW_SCRIPT ?? path.join(PACKAGE_ROOT, "modules", "pidex", "project-pipeline", "scripts", "project-pipeline", "preview.mjs"); const PIDEX_CHILD_ENV = "PIDEX_CHILD"; const PIDEX_SANDBOX_CONTEXT_ENV = "PIDEX_SANDBOX_CONTEXT"; const PIDEX_PROJECT_BOUNDARY_ENV = "PIDEX_PROJECT_BOUNDARY_CONTEXT"; const MAX_STDERR_CHARS = 64 * 1024; const MAX_STDERR_DETAILS_CHARS = 8 * 1024; const MAX_JSON_PARSE_LINE_BYTES = 1024 * 1024; const MAX_LINE_BUFFER_CHARS = 4 * 1024 * 1024; const MAX_TOOL_DETAILS_CHARS = 64 * 1024; const MAX_TOOL_CONTENT_CHARS = 16 * 1024; const MAX_AGENT_FINAL_CHARS = 2000; const MAX_UPDATE_CHARS = 4000; const UPDATE_INTERVAL_MS = 750; const FINAL_STOP_GRACE_MS = 1000; const FINAL_STOP_HARD_KILL_MS = 3000; const BUILTIN_TOOL_NAMES = new Set(["read", "bash", "edit", "write", "grep", "find", "ls"]); const TOOL_HEAVY_AGENTS = new Set([ "pidex-analyst", "pidex-architect", "pidex-planner", "pidex-implementer", "pidex-qa", "pidex-security", "pidex-devops", "pidex-wiki-hygienist", "pidex-pi", ]); const TOOL_FORWARDING_AGENTS = new Set(["pidex-implementer", "pidex-security", "pidex-qa"]); const SANDBOXED_AGENT_NAMES = new Set(["pidex-implementer", "pidex-security", "pidex-qa"]); const TOOL_ALIASES: Record = { glob: "find", grep: "grep", find: "find", ls: "ls", read: "read", write: "write", edit: "edit", bash: "bash", todoread: "read", todo_read: "read", todowrite: "write", todo_write: "write", websearch: "web_search", web_search: "web_search", // User-question tools from running-claude do not exist in Pi direct mode. // Agents must emit a ROUTING gate instead of blocking inside a subprocess. ask: undefined, asktool: undefined, ask_tool: undefined, askagent: undefined, ask_agent: undefined, askuserquestion: undefined, ask_user_question: undefined, askuser: undefined, ask_user: undefined, askquestion: undefined, ask_question: undefined, }; function normalizeToolName(raw: string): string | undefined { const trimmed = raw.trim(); if (!trimmed) return undefined; const lower = trimmed.toLowerCase(); if (lower.startsWith("mcp__")) return lower; const key = lower.replace(/[^a-z0-9_]/g, ""); if (!key) return undefined; const mapped = Object.prototype.hasOwnProperty.call(TOOL_ALIASES, key) ? TOOL_ALIASES[key] : key; if (!mapped) return undefined; if (BUILTIN_TOOL_NAMES.has(mapped)) return mapped; if (mapped.startsWith("mcp__") || mapped.includes("__")) return mapped; if (mapped === "web_search" || mapped === "websearch") return mapped; return undefined; } function uniqueTools(tools: Array): string[] | undefined { const unique = Array.from(new Set(tools.filter((tool): tool is string => Boolean(tool)))); return unique.length > 0 ? unique : undefined; } function parseTools(tools?: string): string[] | undefined { if (!tools) return undefined; return uniqueTools(tools.split(/[,\n]/).map(normalizeToolName)); } function normalizeToolList(tools?: string[]): string[] | undefined { if (!tools) return undefined; return uniqueTools(tools.map(normalizeToolName)); } function hasCustomTools(tools?: string[]): boolean { return Boolean(tools?.some((tool) => !BUILTIN_TOOL_NAMES.has(tool))); } type AutomaticRetrospectiveLearningSource = { store?: any; eligibility?: unknown; tier?: "project" | "global"; scope_id?: string; source_generation?: string; runner_configuration?: unknown; runner?: (input: any) => unknown; fresh_base?: string; now?: string; run?: (input: { finding_bytes: Buffer }) => Promise; close?: () => void; }; function hostRetrospectiveArtifact(result: any, agentCwd: string): Buffer | undefined { if (result?.exitCode !== 0 || extractRoutingField(extractRoutingBlock(String(result?.finalText || "")), "verdict") !== "COMPLETE") return undefined; const contextFile = extractRoutingField(extractRoutingBlock(String(result?.finalText || "")), "context_file"); if (!contextFile || !/^agents\.output\/retrospective\/[a-zA-Z0-9_-]+\.(?:md|json)$/.test(contextFile) || contextFile.includes("..")) return undefined; const relative = `${contextFile.replace(/\.(?:md|json)$/, "")}.rule-learning.json`; try { const root = fs.realpathSync.native(path.join(agentCwd, "agents.output", "retrospective")); const artifact = path.join(agentCwd, ...relative.split("/")); const stat = fs.lstatSync(artifact); const real = fs.realpathSync.native(artifact); if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1 || path.dirname(real) !== root) return undefined; return fs.readFileSync(real); } catch { return undefined; } } /** Reads enrolled ref through one shell-free, output-bounded process operation. */ export function createHostAutomaticLearningProcessAdapter({ spawnProcess = spawn, timeoutMs = 5_000, outputBytes = 4_096 }: any = {}) { return async ({ enrolledRepository, enrolledRemote, branch }: any = {}) => { if (typeof spawnProcess !== "function" || typeof enrolledRepository !== "string" || !enrolledRepository || typeof enrolledRemote !== "string" || !enrolledRemote || typeof branch !== "string" || !branch || /[\r\n\0]/.test(enrolledRepository + enrolledRemote + branch)) return { status: null, stdout: "" }; return await new Promise((resolve) => { let child: any; let settled = false; let stdout = ""; let stderr = ""; const finish = (status: number | null) => { if (!settled) { settled = true; const oid = status === 0 ? normalizeAutomaticLearningLsRemoteOutput({ stdout, branch }) : null; resolve(oid ? { status: 0, stdout: oid } : { status: null, stdout: "" }); } }; try { child = spawnProcess("git", ["-C", enrolledRepository, "ls-remote", "--heads", enrolledRemote, `refs/heads/${branch.replace(/^refs\/heads\//, "")}`], { shell: false, stdio: ["ignore", "pipe", "pipe"], timeout: timeoutMs }); child.stdout?.on("data", (chunk: any) => { stdout += String(chunk); if (Buffer.byteLength(stdout, "utf8") > outputBytes) { try { child.kill("SIGTERM"); } catch {} finish(null); } }); child.stderr?.on("data", (chunk: any) => { stderr += String(chunk); if (Buffer.byteLength(stderr, "utf8") > outputBytes) { try { child.kill("SIGTERM"); } catch {} finish(null); } }); child.once("error", () => finish(null)); child.once("close", (status: number | null) => finish(status)); } catch { finish(null); } }); }; } /** Production host bridge binds configured runner and P1 source-owned state; callers cannot provide store/eligibility/target authority. */ export function createHostAutomaticLearningSource({ root = PACKAGE_ROOT, env = process.env, tier, scope_id, retry_family_id, now = new Date().toISOString(), processAdapter, runConfigured = runConfiguredAgent, openRuntimeSource = openAutomaticLearningRuntimeSource }: any): AutomaticRetrospectiveLearningSource { const runtime = openRuntimeSource({ root, env, tier, scope_id }); const runner = createHostAutomaticLearningRunner({ runConfigured, cwd: root }); const enrolledProcessAdapter = processAdapter ?? createHostAutomaticLearningProcessAdapter(); return Object.freeze({ run: ({ finding_bytes }: { finding_bytes: Buffer }) => runtime.run({ finding_bytes, retry_family_id, runner, processAdapter: enrolledProcessAdapter, now }), close: () => runtime.close(), }); } /** Default host source derives project scope only from confined retrospective bytes; store remints all authority. */ function createDefaultHostAutomaticLearningSource({ finding_bytes }: { finding_bytes: Buffer }): AutomaticRetrospectiveLearningSource | undefined { try { const finding = JSON.parse(finding_bytes.toString("utf8")); if (!/^[a-f0-9]{24,64}$/.test(String(finding?.project_scope_id || "")) || !/^finding:[a-f0-9]{16,64}$/.test(String(finding?.finding_id || ""))) return undefined; return createHostAutomaticLearningSource({ tier: "project", scope_id: finding.project_scope_id, retry_family_id: `retry:host-${createHash("sha256").update(finding_bytes).digest("hex")}` }); } catch { return undefined; } } /** Host-only automatic ingress after one successful retrospective. Factory-owned authority closes after sole disposition. */ /** Plan048 host seam: one kill switch (PIDEX_LIFECYCLE_ACTION_ENABLED) gates automatic lifecycle action after an ordinary terminal result; raw result/cadence bytes never cross this boundary. */ export function runHostLifecycleActionInvocation({ store, result_bytes, result_digest, current, env = process.env, now = new Date().toISOString(), trace = closedLifecycleActionHistoryAdapter, invoker = invokeLifecycleActionFromOrdinaryResult }: any) { if (!env || !env.PIDEX_LIFECYCLE_ACTION_ENABLED) return Object.freeze({ status: "no_op", reason: "kill_switch" }); try { return invoker({ store, result_bytes, result_digest, current, env, now, trace }); } catch { return Object.freeze({ status: "no_op", reason: "action_unavailable" }); } } export function runHostRetrospectiveAutomaticLearning({ result, agentCwd, source, sourceFactory = createDefaultHostAutomaticLearningSource, coordinator = runAutomaticRuleLearningCoordinatorAsync }: { result: any; agentCwd: string; source?: AutomaticRetrospectiveLearningSource; sourceFactory?: (input: { finding_bytes: Buffer }) => AutomaticRetrospectiveLearningSource | undefined; coordinator?: typeof runAutomaticRuleLearningCoordinatorAsync }): any { const finding_bytes = hostRetrospectiveArtifact(result, agentCwd); const resolvedSource = source ?? (finding_bytes ? sourceFactory({ finding_bytes }) : undefined); if (!finding_bytes || !resolvedSource) return Object.freeze({ status: "blocked_artifact_authority" }); if (typeof resolvedSource.run === "function") return Promise.resolve(resolvedSource.run({ finding_bytes })).catch(() => Object.freeze({ status: "blocked_recovery_pending" })).finally(() => resolvedSource.close?.()); if (!resolvedSource.store || typeof resolvedSource.runner !== "function") return Object.freeze({ status: "blocked_artifact_authority" }); return Promise.resolve(coordinator({ ...resolvedSource, finding_bytes })).then((disposition: any) => { if (typeof resolvedSource.store?.appendAutomaticLearningDisposition === "function" && typeof resolvedSource.now === "string" && typeof disposition?.status === "string") { const disposition_id = `automatic-disposition:${createHash("sha256").update(`pidex-automatic-disposition-v1\0${finding_bytes.toString("utf8")}\0${disposition.status}`).digest("hex")}`; try { resolvedSource.store.appendAutomaticLearningDisposition({ disposition_id, status: disposition.status, occurred_at: resolvedSource.now }); } catch { return Object.freeze({ status: "blocked_durable_conflict" }); } } return disposition; }, () => Object.freeze({ status: "blocked_recovery_pending" })).finally(() => resolvedSource.close?.()); } export function shouldDisableChildExtensions(tools?: string[], sandboxContext?: SandboxRuntimeContext): boolean { if (sandboxContext?.mode === "hardened-pipeline") return false; return !hasCustomTools(tools); } function parsePositiveInt(value: string | number | undefined): number | undefined { if (value === undefined) return undefined; const parsed = typeof value === "number" ? value : Number.parseInt(value, 10); return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined; } function loadRoutingConfig(): RoutingConfig { try { if (!fs.existsSync(CONFIG_PATH)) return { defaults: { provider: "pi" }, agents: {}, fallback: { on_error: "pi" } }; return JSON.parse(fs.readFileSync(CONFIG_PATH, "utf8")); } catch (error: any) { throw new Error(`Failed to read pidex config at ${CONFIG_PATH}: ${error?.message ?? error}`); } } function resolveRoute(config: RoutingConfig, agentName: string): AgentRoute { return mergeRoute(config, agentName); } type HostAgentRequest = { agent: string; closeout?: CloseoutRequest; provider?: string; model?: string; effort?: string; laneId?: string; trigger?: string; runFamilyId?: string; planId?: string; reviewGate?: string; reviewMode?: string; attemptId?: string; resumeHoldId?: string; resumeConfirmed?: boolean }; export function validateHostAgentRequestShape(params: HostAgentRequest): { secondary: boolean } { const hasManualRoute = params.provider !== undefined || params.model !== undefined || params.effort !== undefined; if (params.laneId === undefined) { if (hasManualRoute) throw new Error("Host pidex_agent primary calls reject caller-supplied provider, model, or effort."); if (params.trigger !== undefined) throw new Error("Host pidex_agent primary calls cannot include a secondary trigger."); return { secondary: false }; } if (typeof params.laneId !== "string" || !params.laneId.trim()) throw new Error("Host pidex_agent secondary calls require a non-empty laneId."); if (hasManualRoute) throw new Error("Host pidex_agent secondary calls cannot include provider, model, or effort with laneId."); if (typeof params.trigger !== "string" || !params.trigger.trim()) throw new Error("Host pidex_agent secondary calls require trigger with laneId."); return { secondary: true }; } export function resolveHostAgentRoute( params: HostAgentRequest, config: RoutingConfig, eligibleLanes: EligibleParallelLane[], ): HostAgentRoute { const request = validateHostAgentRequestShape(params); if (!request.secondary) { const route = resolveRoute(config, params.agent); return { ...route, provider: normalizeProvider(route.provider), timeoutSeconds: route.timeout_seconds, routeSource: "configured-primary" }; } const lane = eligibleLanes.find((candidate) => candidate.lane_id === params.laneId && candidate.agent === params.agent && candidate.trigger === params.trigger); if (!lane?.runner_provider) throw new Error(`Host pidex_agent lane '${params.laneId}' is not eligible for agent '${params.agent}' and trigger '${params.trigger}'.`); const route = resolveRoute(config, params.agent); return { ...route, provider: normalizeProvider(lane.runner_provider), model: lane.runner_model, effort: lane.effort, timeout_seconds: lane.timeout_seconds, timeoutSeconds: lane.timeout_seconds, routeSource: `configured-secondary:${params.laneId}`, }; } function readJsonObject>(file: string, fallback: T): T { try { if (!fs.existsSync(file)) return fallback; return JSON.parse(fs.readFileSync(file, "utf8")); } catch { return fallback; } } function deepMerge>(base: T, override: Record): T { return mergeSandbox(base, override) as T; } function loadSandboxConfig(): SandboxConfig { const base = readJsonObject(SANDBOX_CONFIG_PATH, { enabled: false, default_mode: "off", profiles: {} }); const local = readJsonObject>(SANDBOX_LOCAL_CONFIG_PATH, {}); return deepMerge(base, local); } function resolveSandboxState(): SandboxState { const config = loadSandboxConfig(); const mode = config.default_mode === "hardened-pipeline" ? "hardened-pipeline" : "off"; const profile = config.profiles?.[mode]; const enabled = config.enabled === true && mode === "hardened-pipeline" && profile?.enabled === true; return { mode: enabled ? "hardened-pipeline" : "off", enabled, reason: enabled ? "config-enabled" : `disabled-or-off(enabled=${config.enabled === true}, default_mode=${config.default_mode ?? "off"}, profile_enabled=${profile?.enabled === true})`, configPath: SANDBOX_CONFIG_PATH, localConfigPath: SANDBOX_LOCAL_CONFIG_PATH, profile, }; } type ProjectPipelineModeResult = { ok: boolean; mode?: "host-direct" | "hardened-pipeline" | "project-pipeline"; source?: string; decision_required?: boolean; reason?: string; choices?: string[]; no_fallback?: boolean; }; export function resolveSandboxStateForProjectMode(projectMode?: ProjectPipelineModeResult): SandboxState { const state = resolveSandboxState(); if (projectMode?.ok && projectMode.mode === "hardened-pipeline") return state; const reasonMode = projectMode?.mode ?? (projectMode?.decision_required ? "decision-required" : "unresolved"); return { ...state, mode: "off", enabled: false, reason: `per-project-mode-${reasonMode}; global ${state.reason}`, }; } function redactedProjectPipelineModeFailure(exitCode: number | null): ProjectPipelineModeResult { return { ok: false, decision_required: true, reason: `project-pipeline mode resolver failed exit=${exitCode}; helper output omitted to avoid local path or credential metadata exposure` }; } export function runProjectPipelineModeResolver(projectRoot: string, mode?: string, source = "interactive"): ProjectPipelineModeResult { if (mode) assertRuntimeBaseline(mode); if (!fs.existsSync(PROJECT_PIPELINE_MODE_SCRIPT)) return { ok: false, decision_required: true, reason: `project-pipeline mode resolver missing; run /pidex-init-home or update the canonical PIDEX runtime before starting /pd` }; const args = [PROJECT_PIPELINE_MODE_SCRIPT, "--pidex-root", PACKAGE_ROOT, "--project-root", projectRoot, "--json"]; if (mode) args.push("--mode", mode, "--source", source); const proc = spawnSync(process.execPath, args, { cwd: PACKAGE_ROOT, encoding: "utf8", timeout: 10_000 }); if (proc.status !== 0) return redactedProjectPipelineModeFailure(proc.status); try { return JSON.parse(proc.stdout || "{}"); } catch { return redactedProjectPipelineModeFailure(proc.status); } } export function saveProjectPipelineMode(projectRoot: string, mode: "host-direct" | "hardened-pipeline" | "project-pipeline", source = "pd-first-run"): ProjectPipelineModeResult { return runProjectPipelineModeResolver(projectRoot, mode, source); } export function projectPipelineModeMissingGuidance(): string { return "Project has no saved PIDEX mode. Run /pdproject use project-pipeline|hardened-pipeline|host-direct from the target project, or start /pd in an interactive Pi UI."; } type RecentPidexProject = { cwd: string; last_ts?: string; last_event?: string; last_mode?: string; }; function upsertRecentProject(byCwd: Map, item: RecentPidexProject): void { const previous = byCwd.get(item.cwd); if (!previous || String(item.last_ts || "") > String(previous.last_ts || "")) byCwd.set(item.cwd, item); } function isTemporaryProjectDirectory(projectRoot: string): boolean { const relative = path.relative(path.resolve(os.tmpdir()), path.resolve(projectRoot)); return relative === "" || (!relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative)); } export function listRecentPidexProjects(limit = 5, stateDir = resolveStateRoot({ root: PACKAGE_ROOT })): RecentPidexProject[] { const byCwd = new Map(); const historyFile = path.join(stateDir, "history.jsonl"); if (fs.existsSync(historyFile)) { for (const line of fs.readFileSync(historyFile, "utf8").split(/\r?\n/)) { if (!line.trim()) continue; try { const row = JSON.parse(line); const cwd = path.resolve(String(row?.cwd || "")); if (!cwd || isTemporaryProjectDirectory(cwd) || !fs.existsSync(cwd) || !fs.statSync(cwd).isDirectory()) continue; upsertRecentProject(byCwd, { cwd, last_ts: String(row?.ts || ""), last_event: row?.event ? String(row.event) : undefined, last_mode: row?.mode ? String(row.mode) : undefined }); } catch { // Ignore malformed history rows; project selection is a convenience, not authority. } } } const modeDir = path.join(stateDir, "project-pipeline-modes"); if (fs.existsSync(modeDir)) { for (const entry of fs.readdirSync(modeDir, { withFileTypes: true })) { if (!entry.isFile() || !entry.name.endsWith(".json")) continue; try { const row = JSON.parse(fs.readFileSync(path.join(modeDir, entry.name), "utf8")); const mode = row?.mode ? String(row.mode) : undefined; const cwd = path.resolve(String(row?.project_root || "")); if (!cwd || isTemporaryProjectDirectory(cwd)) continue; const hostDirExists = fs.existsSync(cwd) && fs.statSync(cwd).isDirectory(); if (!hostDirExists && mode !== "project-pipeline") continue; upsertRecentProject(byCwd, { cwd, last_ts: String(row?.decided_at || ""), last_event: "mode-saved", last_mode: mode }); } catch { // Ignore malformed saved mode rows; project selection is a convenience, not authority. } } } return [...byCwd.values()].sort((a, b) => String(b.last_ts || "").localeCompare(String(a.last_ts || ""))).slice(0, limit); } export const PIDEX_DEFER_PROJECT_SELECTION = "__PIDEX_DEFER_PROJECT_SELECTION__"; export function isLikelyPidexProjectDirectory(projectRoot: string): boolean { const fileOrDirMarkers = [".git", "package.json", "pyproject.toml", "go.mod", "Cargo.toml", "agents.output"]; if (fileOrDirMarkers.some((marker) => fs.existsSync(path.join(projectRoot, marker)))) return true; const pidexContext = path.join(projectRoot, "pidex", "context", "CONTEXT.md"); if (fs.existsSync(pidexContext)) return true; const wikiIndex = path.join(projectRoot, "wiki", "index.md"); return fs.existsSync(wikiIndex); } export function recentPidexProjectsPrompt(limit = 12, stateDir = resolveStateRoot({ root: PACKAGE_ROOT })): string { const recent = listRecentPidexProjects(limit, stateDir); if (recent.length === 0) return "Recent PIDEX projects: none recorded."; const lines = recent.map((item, index) => { const mode = item.last_mode ? ` mode=${item.last_mode}` : ""; const ts = item.last_ts ? ` last=${item.last_ts}` : ""; return `${String.fromCharCode(65 + index)}) ${item.cwd}${mode}${ts}`; }); return `Recent PIDEX projects for interview context:\n${lines.join("\n")}`; } export async function choosePidexProjectRoot(ctx: any): Promise { const current = path.resolve(ctx.cwd ?? process.cwd()); if (!ctx.hasUI) return current; return isLikelyPidexProjectDirectory(current) ? current : PIDEX_DEFER_PROJECT_SELECTION; } const PROJECT_PIPELINE_FIRST_RUN_CONFIRMATION = "Project Pipeline selected. PIDEX will create/open a persistent Docker Project Sandbox for the selected project, import source into /workspace, run Pi inside the container, publish safe agents.output/** and wiki/** to the authoritative archive, and mirror them into the registered host project. Source code is not mirrored back. Mirror degradation is reported explicitly. Failures do not fall back."; export async function chooseProjectPipelineMode(ctx: any, projectRoot: string, options: { saveMode?: typeof saveProjectPipelineMode } = {}): Promise { const optionMap = new Map([ ["host-direct — run normal PIDEX on the host for this project", "host-direct"], ["hardened-pipeline — run normal PIDEX with extra sandbox/runtime hardening", "hardened-pipeline"], ["project-pipeline — run inside Docker; archive then mirror artifacts/wiki into this project", "project-pipeline"], ]); const cancelLabel = "Cancel — do not save a mode"; const choice = await ctx.ui.select("Choose PIDEX mode for this existing project. This is saved per project and controls future /pd routing.", [...optionMap.keys(), cancelLabel]); if (choice === cancelLabel) return undefined; const mode = optionMap.get(choice) ?? (choice as ProjectPipelineMode); if (mode === "project-pipeline") await ctx.ui.notify(PROJECT_PIPELINE_FIRST_RUN_CONFIRMATION, "info"); const saveMode = options.saveMode || saveProjectPipelineMode; return saveMode(projectRoot, mode, "pd-first-run"); } async function resolveProjectPipelineModeForCommand(ctx: any, projectRoot: string): Promise { const current = runProjectPipelineModeResolver(projectRoot); if (!current.decision_required) return current; if (!ctx.hasUI) return current; return chooseProjectPipelineMode(ctx, projectRoot); } export function projectPipelineModeEvidenceLine(result: ProjectPipelineModeResult): string { if (!result.ok && result.decision_required) return `project_pipeline_mode: decision-required; reason: ${result.reason ?? "missing saved mode"}`; return `project_pipeline_mode: ${result.mode ?? "host-direct"}; source: ${result.source ?? "unknown"}; no_fallback: ${result.no_fallback === true}`; } export function projectPipelineModeInstructionLine(result: ProjectPipelineModeResult): string { return result.mode === "project-pipeline" ? "Project Pipeline mode is explicit and fail-closed: do not fall back to host-direct or hardened-pipeline automatically. Use the project-pipeline.orchestrator facade for end-to-end create/open, source import/clone, selected credential bootstrap, sequential child Pi execution in the container, and archive sync. Safe agents.output/** and wiki/** publish to the authoritative archive and then mirror into the registered host project; do not mirror source code back to the host." : "Project Pipeline mode is not active for this project; existing host-direct/hardened-pipeline behavior remains unchanged."; } export function projectModeForTelemetry(projectRoot: string): ProjectPipelineMode | undefined { try { const resolved = runProjectPipelineModeResolver(projectRoot); return resolved.ok && resolved.mode ? resolved.mode : undefined; } catch { return undefined; } } type ProjectPipelineRunFlowRequest = { projectRoot: string; task: string; copyPiCredentials?: boolean; acknowledgeTrustedPersistentContainer?: boolean; }; type ProjectPipelineRunFlowResult = { ok: boolean; exitCode?: number | null; projectId?: string; stdout?: string; stderr?: string; error?: string; no_fallback: true; }; export function isProjectPipelineUiPreviewTask(task?: string): boolean { return /\b(ui|frontend|front-end|browser|page|dashboard|component|css|visual|vite|react|vue|svelte)\b/i.test(String(task || "")); } export const DEFAULT_PROJECT_PIPELINE_PREVIEW_COMMAND = ["pnpm", "exec", "vite", "--host", "0.0.0.0", "--port", "$PORT"] as const; function slugForProjectPipelineId(projectRoot: string): string { const base = path.basename(path.resolve(projectRoot)).toLowerCase().replace(/[^a-z0-9_.-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 32) || "project"; const hash = createHash("sha256").update(path.resolve(projectRoot)).digest("hex").slice(0, 12); return `pp-${base}-${hash}`.slice(0, 80); } export function buildProjectPipelineRunFlowArgs(request: ProjectPipelineRunFlowRequest): { script: string; projectId: string; args: string[] } { const projectRoot = path.resolve(request.projectRoot || process.cwd()); const task = String(request.task || "").trim(); if (!task) throw new Error("Project Pipeline run-flow requires an initial task; no host-direct fallback is allowed."); if (!fs.existsSync(PROJECT_PIPELINE_ORCHESTRATOR_SCRIPT)) throw new Error(`project-pipeline orchestrator helper missing at ${PROJECT_PIPELINE_ORCHESTRATOR_SCRIPT}; run /pidex-init-home or update the canonical PIDEX runtime before starting /pd`); const projectId = slugForProjectPipelineId(projectRoot); const args = [ PROJECT_PIPELINE_ORCHESTRATOR_SCRIPT, "--pidex-root", PACKAGE_ROOT, "--project-id", projectId, "--source", projectRoot, "--task", [ "Project Pipeline /pd entrypoint. You are running inside the persistent Project Sandbox at /workspace.", "Run the configured in-container PIDEX phase chain for the user's task. Do not run host-direct or hardened-pipeline fallback.", "Each phase must write its full artifact under agents.output/** and finish with a ROUTING block whose context_file points to that artifact.", `Initial user task: ${task}`, ].join("\n\n"), "--json", ]; if (request.copyPiCredentials) { if (!request.acknowledgeTrustedPersistentContainer) throw new Error("copying Pi credentials into Project Pipeline requires trusted persistent container acknowledgement"); args.splice(args.length - 1, 0, "--pi-auth", path.join(os.homedir(), ".pi", "agent", "auth.json"), "--pi-settings", path.join(os.homedir(), ".pi", "agent", "settings.json"), "--acknowledge-trusted-persistent-container"); } return { script: PROJECT_PIPELINE_ORCHESTRATOR_SCRIPT, projectId, args }; } function safeProjectMirrorSummary(mirror: any): any { if (!mirror) return undefined; return { status: mirror?.status, degraded: mirror?.degraded === true, copied: Number(mirror?.copied || 0), updated: Number(mirror?.updated || 0), deleted: Number(mirror?.deleted || 0), conflicts: Number(mirror?.conflicts || 0), conflict_paths: Array.isArray(mirror?.conflict_paths) ? mirror.conflict_paths.map(safeProjectMirrorPath).filter(Boolean).slice(0, 20) : [], }; } function safeReviewCompletionSummary(value: any): any { const status = String(value?.status || ""); if (!["accepted", "CHANGES_REQUESTED", "USER_DECISION_REQUIRED", "CLOSED_WITH_TBR", "resumed"].includes(status)) return undefined; return { status, ...(typeof value?.disposition === "string" ? { disposition: value.disposition } : {}), ...(Array.isArray(value?.tbrIds) ? { tbrIds: value.tbrIds.filter((id: any) => /^TBR-[a-f0-9]{12}$/.test(String(id))).slice(0, 20) } : {}), }; } function safeProjectPipelineHelperOutput(rawStdout: string, projectId: string, exitCode: number | null): { stdout: string; error?: string } { try { const parsed = JSON.parse(rawStdout || "{}"); const runs = Array.isArray(parsed?.runs) ? parsed.runs.map((run: any) => ({ agent: run?.agent, ok: run?.ok === true, context_file: run?.context_file, archive_sync_status: run?.archive_sync_status, project_mirror: safeProjectMirrorSummary(run?.project_mirror), sync_degraded: run?.sync_degraded === true, error: run?.error, })) : undefined; const safe = { ok: parsed?.ok === true, no_fallback: parsed?.no_fallback === true, error: parsed?.error, failed_agent: parsed?.failed_agent, lifecycle: { record: { project_id: parsed?.lifecycle?.record?.project_id ?? projectId } }, final_context_file: parsed?.final_context_file, final_archive_context_file: parsed?.final_archive_context_file, latest_project_mirror_status: parsed?.latest_project_mirror_status, any_mirror_degraded: parsed?.any_mirror_degraded === true, runs, }; const error = safe.ok ? undefined : `Project Pipeline orchestrator failed closed (no fallback). error=${safe.error ?? "unknown"}${safe.failed_agent ? ` failed_agent=${safe.failed_agent}` : ""} exit=${exitCode}`; return { stdout: JSON.stringify(safe), error }; } catch { return { stdout: "", error: `Project Pipeline orchestrator failed closed (no fallback). exit=${exitCode}; helper output omitted` }; } } export function runProjectPipelineRunFlow(request: ProjectPipelineRunFlowRequest): ProjectPipelineRunFlowResult { let built: { projectId: string; args: string[] }; try { built = buildProjectPipelineRunFlowArgs(request); } catch (error: any) { return { ok: false, error: error?.message ?? String(error), no_fallback: true }; } const proc = spawnSync(process.execPath, built.args, { cwd: PACKAGE_ROOT, encoding: "utf8", timeout: 30 * 60_000, maxBuffer: 20 * 1024 * 1024 }); const safe = safeProjectPipelineHelperOutput(proc.stdout || "", built.projectId, proc.status); return { ok: proc.status === 0, exitCode: proc.status, projectId: built.projectId, stdout: safe.stdout, stderr: "", error: proc.status === 0 ? undefined : safe.error, no_fallback: true }; } function projectPipelineProgressMessage(line: string): string | undefined { const match = String(line || "").match(/^\[pidex:project-pipeline\]\s*(.+)$/); return match?.[1]?.trim(); } export async function runProjectPipelineRunFlowAsync(request: ProjectPipelineRunFlowRequest, options: { onProgress?: (message: string) => void; timeoutMs?: number } = {}): Promise { let built: { projectId: string; args: string[] }; try { built = buildProjectPipelineRunFlowArgs(request); } catch (error: any) { return { ok: false, error: error?.message ?? String(error), no_fallback: true }; } return await new Promise((resolve) => { const timeoutMs = options.timeoutMs ?? 30 * 60_000; const child = spawn(process.execPath, built.args, { cwd: PACKAGE_ROOT, stdio: ["ignore", "pipe", "pipe"] }); let stdout = ""; let stderr = ""; let stderrRemainder = ""; let settled = false; const finish = (result: ProjectPipelineRunFlowResult) => { if (settled) return; settled = true; clearTimeout(timer); process.off("SIGINT", onSigint); resolve(result); }; const onSigint = () => { try { child.kill("SIGINT"); } catch {} setTimeout(() => { try { if (!child.killed) child.kill("SIGTERM"); } catch {} }, 2000).unref?.(); finish({ ok: false, exitCode: null, projectId: built.projectId, stdout: "", stderr: "", error: "Project Pipeline interrupted by user; child process was signalled", no_fallback: true }); }; const timer = setTimeout(() => { try { child.kill("SIGTERM"); } catch {} finish({ ok: false, exitCode: null, projectId: built.projectId, stdout: "", stderr: "", error: `Project Pipeline timed out after ${timeoutMs}ms; child process was terminated`, no_fallback: true }); }, timeoutMs); process.once("SIGINT", onSigint); child.stdout?.on("data", (chunk) => { stdout += String(chunk); }); child.stderr?.on("data", (chunk) => { stderr += String(chunk); stderrRemainder += String(chunk); const parts = stderrRemainder.split(/\r?\n/); stderrRemainder = parts.pop() || ""; for (const part of parts) { const progress = projectPipelineProgressMessage(part); if (progress) options.onProgress?.(progress); } }); child.on("error", (error: any) => finish({ ok: false, exitCode: null, projectId: built.projectId, stdout: "", stderr: "", error: error?.message ?? String(error), no_fallback: true })); child.on("close", (code) => { if (stderrRemainder) { const progress = projectPipelineProgressMessage(stderrRemainder); if (progress) options.onProgress?.(progress); } const safe = safeProjectPipelineHelperOutput(stdout || "", built.projectId, code); finish({ ok: code === 0, exitCode: code, projectId: built.projectId, stdout: safe.stdout, stderr: "", error: code === 0 ? undefined : safe.error, no_fallback: true }); }); }); } export function startDefaultProjectPipelinePreview(projectId: string): { ok: boolean; summary: string; no_fallback?: true } { return runPdProjectCommand({ command: "preview", action: "start", projectId, commandArgs: [...DEFAULT_PROJECT_PIPELINE_PREVIEW_COMMAND] }, {}); } export function summarizeProjectPipelineRunFlowResult(result: ProjectPipelineRunFlowResult): string { const fallback = `Project Pipeline run-flow complete for ${result.projectId ?? "unknown-project"}; no_fallback=${result.no_fallback === true}.`; if (!result.stdout) return fallback; try { const parsed = JSON.parse(result.stdout); const projectId = String(parsed?.lifecycle?.record?.project_id || result.projectId || "unknown-project"); const finalRun = Array.isArray(parsed?.runs) ? parsed.runs.at(-1) : undefined; const contextFile = parsed?.final_context_file || finalRun?.context_file || parsed?.run?.context_file || parsed?.run?.routing?.context_file || parsed?.archive_context_file; const archiveStatus = finalRun?.archive_sync_status || parsed?.run?.archive_sync_status || (parsed?.run?.archiveSyncReport?.ok === true ? "complete" : undefined); const mirrorStatus = parsed?.latest_project_mirror_status || finalRun?.project_mirror?.status; const syncDegraded = parsed?.any_mirror_degraded === true || finalRun?.project_mirror?.degraded === true; const noFallback = parsed?.no_fallback === true || result.no_fallback === true; return [ `Project Pipeline run-flow complete for ${projectId}.`, contextFile ? `context_file: ${contextFile}` : undefined, archiveStatus ? `archive_sync: ${archiveStatus}` : undefined, mirrorStatus ? `project_mirror: ${mirrorStatus}` : undefined, syncDegraded ? "archive complete; project mirror degraded" : undefined, `no_fallback: ${noFallback}`, ].filter(Boolean).join(" "); } catch { return fallback; } } type PdProjectCommand = | { command: "help" } | { command: "use"; mode: "host-direct" | "hardened-pipeline" | "project-pipeline" } | { command: "status"; projectId?: string } | { command: "diagnose"; projectId: string } | { command: "runs"; projectId: string } | { command: "show-run"; projectId: string; runId: string } | { command: "artifacts"; projectId: string } | { command: "open"; projectId: string } | { command: "repair"; projectId: string; confirm: string } | { command: "upgrade-pi"; projectId: string; confirm: string } | { command: "credentials"; action: "status" | "reset"; projectId: string; confirm?: string } | { command: "preview"; action: "start" | "status" | "logs" | "stop"; projectId: string; commandArgs?: string[] } | { command: "remove"; projectId: string; confirm: string }; function readPdProjectFlagValue(parts: string[], index: number, flag: string): { value: string; nextIndex: number } { const value = parts[index + 1]; if (!value || value.startsWith("--")) throw new Error(`pdproject ${flag} requires a value`); return { value, nextIndex: index + 1 }; } export function parsePdProjectArgs(argsLine?: string): PdProjectCommand { const parts = String(argsLine || "").trim().split(/\s+/).filter(Boolean); if (parts.length === 0 || parts[0] === "help" || parts[0] === "--help" || parts[0] === "-h") return { command: "help" }; const command = parts.shift(); if (command === "use") { const mode = parts.shift(); if (mode !== "host-direct" && mode !== "hardened-pipeline" && mode !== "project-pipeline") throw new Error("pdproject use requires host-direct, hardened-pipeline, or project-pipeline"); if (parts.length) throw new Error(`unknown pdproject use argument: ${parts[0]}`); return { command: "use", mode }; } if (command === "preview") { const raw = String(argsLine || "").trim(); const separator = raw.indexOf(" -- "); const prefixRaw = separator === -1 ? raw : raw.slice(0, separator); const tailRaw = separator === -1 ? "" : raw.slice(separator + 4); const previewParts = prefixRaw.split(/\s+/).filter(Boolean).slice(1); const action = previewParts.shift(); if (action !== "start" && action !== "status" && action !== "logs" && action !== "stop") throw new Error("pdproject preview requires start, status, logs, or stop"); let projectId = ""; for (let i = 0; i < previewParts.length; i += 1) { if (previewParts[i] === "--project-id") { const read = readPdProjectFlagValue(previewParts, i, "--project-id"); projectId = read.value; i = read.nextIndex; } else if (!projectId && !previewParts[i].startsWith("--")) projectId = previewParts[i]; else throw new Error(`unknown pdproject preview argument: ${previewParts[i]}`); } if (!projectId) throw new Error(`pdproject preview ${action} requires a project id`); if (action === "start") { if (!tailRaw.trim()) throw new Error("pdproject preview start requires -- command"); return { command: "preview", action, projectId, commandArgs: tailRaw.trim().split(/\s+/).filter(Boolean) }; } if (tailRaw.trim()) throw new Error(`pdproject preview ${action} does not accept a command`); return { command: "preview", action, projectId }; } if (command === "status" || command === "diagnose") { let projectId: string | undefined; for (let i = 0; i < parts.length; i += 1) { if (parts[i] === "--project-id") { const read = readPdProjectFlagValue(parts, i, "--project-id"); projectId = read.value; i = read.nextIndex; } else if (!projectId && !parts[i].startsWith("--")) projectId = parts[i]; else throw new Error(`unknown pdproject ${command} argument: ${parts[i]}`); } if (command === "diagnose") { if (!projectId) throw new Error("pdproject diagnose requires a project id"); return { command: "diagnose", projectId }; } return { command: "status", projectId }; } if (command === "runs" || command === "show-run" || command === "artifacts") { let projectId = ""; let runId = ""; for (let i = 0; i < parts.length; i += 1) { if (parts[i] === "--project-id") { if (projectId) throw new Error(`pdproject ${command} received duplicate project id`); const read = readPdProjectFlagValue(parts, i, "--project-id"); projectId = read.value; i = read.nextIndex; } else if (parts[i] === "--run-id") { if (command !== "show-run") throw new Error(`unknown pdproject ${command} argument: ${parts[i]}`); if (runId) throw new Error("pdproject show-run received duplicate run id"); const read = readPdProjectFlagValue(parts, i, "--run-id"); runId = read.value; i = read.nextIndex; } else if (!parts[i].startsWith("--")) { if (!projectId) projectId = parts[i]; else if (command === "show-run" && !runId) runId = parts[i]; else if (command === "show-run") throw new Error("pdproject show-run received duplicate run id"); else throw new Error(`pdproject ${command} received duplicate project id`); } else throw new Error(`unknown pdproject ${command} argument: ${parts[i]}`); } if (!projectId) throw new Error(`pdproject ${command} requires a project id`); if (command === "show-run") { if (!runId) throw new Error("pdproject show-run requires a run id"); return { command, projectId, runId }; } if (runId) throw new Error(`unknown pdproject ${command} argument: --run-id`); return { command, projectId }; } if (command === "open" || command === "repair" || command === "upgrade-pi") { let projectId = ""; let confirm = ""; for (let i = 0; i < parts.length; i += 1) { if (parts[i] === "--project-id") { const read = readPdProjectFlagValue(parts, i, "--project-id"); projectId = read.value; i = read.nextIndex; } else if (parts[i] === "--confirm") { const read = readPdProjectFlagValue(parts, i, "--confirm"); confirm = read.value; i = read.nextIndex; } else if (!projectId && !parts[i].startsWith("--")) projectId = parts[i]; else throw new Error(`unknown pdproject ${command} argument: ${parts[i]}`); } if (!projectId) throw new Error(`pdproject ${command} requires a project id`); if (command === "repair" || command === "upgrade-pi") { if (confirm !== projectId) throw new Error(`pdproject ${command} requires --confirm ${projectId}`); return { command, projectId, confirm }; } if (confirm) throw new Error("unknown pdproject open argument: --confirm"); return { command: "open", projectId }; } if (command === "credentials") { const action = parts.shift(); if (action !== "status" && action !== "reset") throw new Error("pdproject credentials requires status or reset"); let projectId = ""; let confirm = ""; for (let i = 0; i < parts.length; i += 1) { if (parts[i] === "--project-id") { const read = readPdProjectFlagValue(parts, i, "--project-id"); projectId = read.value; i = read.nextIndex; } else if (parts[i] === "--confirm") { const read = readPdProjectFlagValue(parts, i, "--confirm"); confirm = read.value; i = read.nextIndex; } else if (!projectId && !parts[i].startsWith("--")) projectId = parts[i]; else throw new Error(`unknown pdproject credentials argument: ${parts[i]}`); } if (!projectId) throw new Error(`pdproject credentials ${action} requires a project id`); if (action === "reset" && confirm !== projectId) throw new Error(`pdproject credentials reset requires --confirm ${projectId}`); return action === "reset" ? { command: "credentials", action, projectId, confirm } : { command: "credentials", action, projectId }; } if (command === "remove") { let projectId = ""; let confirm = ""; for (let i = 0; i < parts.length; i += 1) { if (parts[i] === "--project-id") { const read = readPdProjectFlagValue(parts, i, "--project-id"); projectId = read.value; i = read.nextIndex; } else if (parts[i] === "--confirm") { const read = readPdProjectFlagValue(parts, i, "--confirm"); confirm = read.value; i = read.nextIndex; } else if (!projectId && !parts[i].startsWith("--")) projectId = parts[i]; else throw new Error(`unknown pdproject remove argument: ${parts[i]}`); } if (!projectId) throw new Error("pdproject remove requires a project id"); if (confirm !== projectId) throw new Error(`pdproject remove requires --confirm ${projectId}`); return { command: "remove", projectId, confirm }; } throw new Error(`unknown pdproject command: ${command}`); } export function pdProjectUsage(): string { return [ "Usage: /pdproject use project-pipeline|hardened-pipeline|host-direct", " /pdproject status [project-id|--project-id ID]", " /pdproject diagnose ", " /pdproject runs ", " /pdproject show-run ", " /pdproject artifacts ", " /pdproject open ", " /pdproject repair --confirm ", " /pdproject upgrade-pi --confirm ", " /pdproject credentials status ", " /pdproject credentials reset --confirm ", " /pdproject preview start -- ", " /pdproject preview status|logs|stop ", " /pdproject remove --confirm ", "", "Project Pipeline sandboxes are persistent. Removal is explicit and irreversible for the Docker container/volumes.", ].join("\n"); } function safeAgentsOutputPath(value: any): string | undefined { const text = String(value || "").replaceAll("\\", "/"); if (!text.startsWith("agents.output/") || text.includes("..")) return undefined; return text; } function safeProjectMirrorPath(value: any): string | undefined { const text = String(value || "").replaceAll("\\", "/"); if (text.includes("..") || (!text.startsWith("agents.output/") && !text.startsWith("wiki/"))) return undefined; return text; } function summarizeProjectRunLine(projectId: string, run: any, details = false): string { return [ `${projectId}: run=${run?.project_run_id ?? "unknown"}`, run?.agent ? `agent=${run.agent}` : undefined, `status=${run?.archive_sync_status ?? "unknown"}`, run?.project_mirror_status ? `mirror=${run.project_mirror_status}${run?.project_mirror_degraded ? ":degraded" : ""}` : undefined, run?.exit_code === undefined ? undefined : `exit=${run.exit_code}`, safeAgentsOutputPath(run?.context_file) ? `context=${safeAgentsOutputPath(run.context_file)}` : undefined, details && run?.archive_context_file ? "archive_context=available" : undefined, run?.started_at ? `started=${run.started_at}` : undefined, run?.ended_at ? `ended=${run.ended_at}` : undefined, ].filter(Boolean).join(" "); } function summarizeProjectRuns(project: any): string { const runs = Array.isArray(project?.runs) ? project.runs : []; if (!runs.length) return `${project?.project_id ?? "unknown-project"}: no Project Pipeline runs recorded.`; return runs.slice(-10).map((run: any) => summarizeProjectRunLine(project.project_id, run)).join("\n"); } function summarizeProjectRun(project: any, runId: string): string { const runs = Array.isArray(project?.runs) ? project.runs : []; const run = runs.find((candidate: any) => candidate?.project_run_id === runId); if (!run) return `${project?.project_id ?? "unknown-project"}: run=${runId} not found.`; return summarizeProjectRunLine(project?.project_id ?? "unknown-project", run, true); } function safePdProjectId(value: string): string { const projectId = String(value || ""); if (!/^[A-Za-z0-9_.-]+$/.test(projectId) || projectId === "." || projectId === ".." || projectId.includes("..")) throw new Error("invalid project id"); return projectId; } function collectProjectArchiveArtifacts(projectId: string): string[] { const safeId = safePdProjectId(projectId); const archiveBase = path.join(PACKAGE_ROOT, "state", "project-archives"); const archiveRoot = path.join(archiveBase, safeId); if (!fs.existsSync(archiveRoot)) return []; const rootStat = fs.lstatSync(archiveRoot); if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) return []; const out: string[] = []; const roots = ["agents.output", "wiki"]; const walk = (relativeDir: string) => { if (out.length >= 50) return; const absoluteDir = path.join(archiveRoot, relativeDir); if (!absoluteDir.startsWith(`${archiveRoot}${path.sep}`) && absoluteDir !== archiveRoot) return; if (!fs.existsSync(absoluteDir)) return; const dirStat = fs.lstatSync(absoluteDir); if (!dirStat.isDirectory() || dirStat.isSymbolicLink()) return; for (const name of fs.readdirSync(absoluteDir).sort()) { if (out.length >= 50) break; const rel = path.posix.join(relativeDir.replaceAll("\\", "/"), name); if (rel.includes("..") || (!rel.startsWith("agents.output/") && rel !== "agents.output" && !rel.startsWith("wiki/") && rel !== "wiki")) continue; const abs = path.join(archiveRoot, rel); const stat = fs.lstatSync(abs); if (stat.isSymbolicLink()) continue; if (stat.isDirectory()) walk(rel); else if (stat.isFile()) out.push(`${rel} size=${stat.size}`); } }; for (const root of roots) walk(root); return out; } function summarizeProjectArtifacts(projectId: string): string { const artifacts = collectProjectArchiveArtifacts(projectId); if (!artifacts.length) return `${projectId}: no archived Project Pipeline artifacts found.`; return artifacts.map((artifact) => `${projectId}: artifact=${artifact}`).join("\n"); } function summarizeProjectCredentials(projectId: string, credentials: any): string { const providers = Array.isArray(credentials?.providers) && credentials.providers.length ? credentials.providers.join(",") : "none"; const inventoryCount = Array.isArray(credentials?.inventory) ? credentials.inventory.length : 0; return `${projectId}: credentials(pi=${credentials?.pi ?? "unknown"}, git=${credentials?.git ?? "unknown"}, providers=${providers}, files=${inventoryCount})`; } function summarizeProjectRecords(projects: any[]): string { if (!projects.length) return "Project Pipeline: no registered local Project Sandboxes."; return projects.map((project: any) => { const runs = Array.isArray(project.runs) ? project.runs.length : 0; const archive = project.archive?.path ? ` archive=${project.archive.path}` : ""; const dockerHealth = project.docker_health?.container ? ` docker=${project.docker_health.container.exists ? project.docker_health.container.status : "missing"}` : ""; const mirror = project?.project_mirror?.status ? ` mirror=${project.project_mirror.status}${project.project_mirror.degraded ? ":degraded" : ""}` : ""; return `${project.project_id}: status=${project.status ?? "unknown"}${dockerHealth} source=${project.source?.kind ?? "unknown"} credentials(pi=${project.credentials?.pi ?? "unknown"}, git=${project.credentials?.git ?? "unknown"}) runs=${runs}${mirror}${archive}`; }).join("\n"); } function dashboardDbContainsProject(project: any): boolean | undefined { const dbPath = path.join(PACKAGE_ROOT, "dashboard", "data", "pidex.sqlite"); if (!fs.existsSync(dbPath)) return undefined; try { const bytes = fs.readFileSync(dbPath); for (const needle of [project?.project_id, project?.name, project?.source?.ref, project?.archive?.path].filter(Boolean).map(String)) { if (needle.length >= 3 && bytes.includes(Buffer.from(needle))) return true; } return false; } catch { return undefined; } } function archiveExistsForProject(project: any): boolean { const archive = project?.archive?.path || path.join(PACKAGE_ROOT, "state", "project-archives", safePdProjectId(project?.project_id || "unknown")); try { return fs.existsSync(archive) && fs.lstatSync(archive).isDirectory(); } catch { return false; } } export function summarizeUpgradeIdentity(assessment: any): string { const allowed = ["id_format", "registered_name", "project_label", "kind_label", "sandbox_label", "container_marker", "running", "unpaused", "loader_environment", "startup_command", "mount_workspace", "mount_pidex_secrets", "mount_cache", "runtime_overlays"]; if (!assessment) return "upgrade_identity: not checked"; const failed = Array.isArray(assessment.failed_checks) ? [...allowed, "inspection_unavailable"].filter(key => assessment.failed_checks.includes(key)) : []; const startup = ["direct-sleep-infinity", "node-entrypoint-sleep-infinity", "other"].includes(assessment.startup_kind) ? assessment.startup_kind : "unknown"; const passed = assessment.ok === true && failed.length === 0 && startup !== "other" && startup !== "unknown" && allowed.every(key => assessment.checks?.[key] === true); return `upgrade_identity: ${passed ? "pass" : failed.length ? "blocked" : "unknown"}; ${failed.length ? `failed_checks=${failed.join(",")} (expected match/true, observed mismatch/false)` : "no failed predicates reported"}; startup=${startup}; expected startup=direct-sleep-infinity or node-entrypoint-sleep-infinity`; } function summarizeProjectDiagnose(project: any): string { const projectId = project?.project_id || "unknown-project"; const runs = Array.isArray(project?.runs) ? project.runs : []; const docker = project?.docker_health?.container; const volumes = project?.docker_health?.volumes || {}; const volumeSummary = Object.entries(volumes).map(([kind, value]: [string, any]) => `${kind}=${value?.exists ? "ok" : "missing"}`).join(", ") || "not checked"; const dashboardVisible = dashboardDbContainsProject(project); const archiveOk = archiveExistsForProject(project); const maintenance = project?.pi_maintenance; const maintenanceStatus = !maintenance ? "not-reported" : ["none", "in_progress", "held", "verified", "failed_unchanged", "rolled_back"].includes(maintenance.status) ? maintenance.status : "unknown"; const executionLock = ["present", "absent"].includes(maintenance?.execution_lock) ? maintenance.execution_lock : "unknown"; const maintenanceHeld = ["in_progress", "held", "unknown"].includes(maintenanceStatus) || executionLock === "present"; const observedPi = /^\d+\.\d+\.\d+$/.test(maintenance?.after || "") ? maintenance.after : "unknown"; const lines = [ `Project Pipeline diagnosis for ${projectId}`, `registry: found name=${project?.name || "unknown"} status=${project?.status || "unknown"}`, `docker: container=${docker ? (docker.exists ? docker.status || "exists" : "missing") : "not checked"}; volumes=${volumeSummary}`, `source: kind=${project?.source?.kind || "unknown"}${project?.source?.ref ? ` ref=${project.source.ref}` : ""}`, `credentials: pi=${project?.credentials?.pi || "unknown"} git=${project?.credentials?.git || "unknown"}`, `runs: ${runs.length}`, `pi_maintenance: ${maintenanceStatus}; execution_lock=${executionLock}; last_observed_version=${observedPi} (recorded, not a live probe)`, summarizeUpgradeIdentity(project?.docker_health?.upgrade_identity), `archive: ${archiveOk ? "present" : "missing"}${project?.archive?.path ? ` path=${project.archive.path}` : ""}`, `project_mirror: ${project?.project_mirror?.status || "unknown"}${project?.project_mirror?.degraded ? " degraded" : ""}`, `dashboard_db: ${dashboardVisible === undefined ? "missing/not-readable" : dashboardVisible ? "project string present" : "project string not found"}`, "", "No-Bash note: this command uses PIDEX Node helpers and Docker CLI directly. It is intended for native Windows sessions where the Pi bash tool cannot start /bin/bash.", "", "Next actions:", ]; if (maintenanceHeld) lines.push("- Maintenance/execution state requires inspection; do not delete locks or automatically restart the pipeline."); if (!maintenanceHeld && (!docker?.exists || docker.status === "missing")) lines.push(`- Repair/open Docker sandbox: /pdproject repair ${projectId} --confirm ${projectId}`); if (!archiveOk) lines.push("- Archive missing: inspect failed runs/HOLD before authorizing any new Project Pipeline run."); if (dashboardVisible === false || dashboardVisible === undefined) lines.push("- Refresh dashboard DB without Bash from the PIDEX root: node dashboard/start.mjs (or run: node scripts/dashboard/ingest.mjs --db dashboard/data/pidex.sqlite --project .)"); lines.push(`- Inspect registry/docker status: /pdproject status ${projectId}`); lines.push(`- Inspect runs: /pdproject runs ${projectId}`); lines.push(`- Inspect archive files: /pdproject artifacts ${projectId}`); return lines.join("\n"); } export function summarizePreviewStart(result: any): string { if (!result?.ok) { if (result?.error_category === "preview_operator_host_unknown") return "Preview port is bound, but PIDEX could not determine the host/IP to show your browser. Set PIDEX_PROJECT_PIPELINE_PREVIEW_HOST and retry."; if (result?.error_category === "preview_port_not_listening") return `Preview process started but did not become reachable on the assigned port before timeout. Check bounded logs with /pdproject preview logs ${result?.project_id ?? ""}.`; if (result?.error_category === "preview_process_exited") return `Preview process exited before becoming reachable. Check bounded logs with /pdproject preview logs ${result?.project_id ?? ""}.`; if (/^preview_(port|container|reassign|recreate)/.test(result?.error_category || "")) return `Preview could not safely reserve/publish a local port range for this Project Pipeline sandbox (${result.error_category}). No fallback was used.`; return `Preview failed (${result?.error_category || "preview_failed"}). No fallback was used.`; } return [ `Preview ready for ${result.project_id}:`, result.operator_url, result.host_bind === "0.0.0.0" ? (result.exposure_note || "Exposure: preview is bound to all interfaces on this Docker host. PIDEX did not open firewalls or tunnels.") : undefined, "", `Logs: /pdproject preview logs ${result.project_id}`, `Stop: /pdproject preview stop ${result.project_id}`, ].filter((line) => line !== undefined).join("\n"); } function summarizePreviewStatusLike(action: string, result: any, projectId: string): string { if (!result?.ok) return `Project Pipeline preview ${action} failed; helper details omitted`; if (action === "logs") return String(result.log_excerpt || `${result.project_id ?? projectId}: no bounded preview logs available.`).slice(0, 4000); return `${result.project_id ?? projectId}: preview status=${result.status ?? "unknown"}${result.operator_url ? ` url=${result.operator_url}` : ""}`; } export function buildPdProjectCommandFromToolParams(params: any): PdProjectCommand { const command = String(params?.command || '').trim(); const projectId = params?.projectId ? String(params.projectId).trim() : ''; const runId = params?.runId ? String(params.runId).trim() : ''; const requireProject = () => { if (!projectId) throw new Error(`pidex_project ${command} requires projectId`); return projectId; }; if (command === 'status') return projectId ? { command: 'status', projectId } : { command: 'status' }; if (command === 'diagnose') return { command: 'diagnose', projectId: requireProject() }; if (command === 'runs') return { command: 'runs', projectId: requireProject() }; if (command === 'artifacts') return { command: 'artifacts', projectId: requireProject() }; if (command === 'credentials-status') return { command: 'credentials', action: 'status', projectId: requireProject() }; if (command === 'preview-status') return { command: 'preview', action: 'status', projectId: requireProject() }; if (command === 'preview-logs') return { command: 'preview', action: 'logs', projectId: requireProject() }; if (command === 'show-run') { if (!runId) throw new Error('pidex_project show-run requires runId'); return { command: 'show-run', projectId: requireProject(), runId }; } throw new Error(`unsupported pidex_project command: ${command || 'missing'}`); } export function runPdProjectCommand(parsed: PdProjectCommand, options: { projectRoot?: string } = {}): { ok: boolean; summary: string; no_fallback?: true } { if (parsed.command === "help") return { ok: true, summary: pdProjectUsage() }; if (parsed.command === "use") { if (!fs.existsSync(PROJECT_PIPELINE_MODE_SCRIPT)) return { ok: false, summary: "project-pipeline mode helper missing; run /pidex-init-home or update the canonical PIDEX runtime" }; const projectRoot = options.projectRoot ?? process.cwd(); const args = [PROJECT_PIPELINE_MODE_SCRIPT, "--pidex-root", PACKAGE_ROOT, "--project-root", projectRoot, "--mode", parsed.mode, "--source", "pdproject-use", "--json"]; const proc = spawnSync(process.execPath, args, { cwd: PACKAGE_ROOT, encoding: "utf8", timeout: 10_000, maxBuffer: 1024 * 1024 }); try { const json = JSON.parse(proc.stdout || "{}"); const ok = proc.status === 0 && json.ok === true && json.mode === parsed.mode; return { ok, no_fallback: parsed.mode === "project-pipeline" ? true : undefined, summary: ok ? `Project Pipeline mode saved for current project: ${parsed.mode}${json.no_fallback === true ? " no_fallback=true" : ""}` : "Project Pipeline mode save failed; helper details omitted" }; } catch { return { ok: false, summary: `project-pipeline use failed exit=${proc.status}; helper output omitted to avoid local path exposure` }; } } if (parsed.command === "artifacts") { try { return { ok: true, summary: summarizeProjectArtifacts(parsed.projectId) }; } catch { return { ok: false, summary: "Project Pipeline artifacts failed: invalid project id" }; } } if (parsed.command === "status" || parsed.command === "diagnose" || parsed.command === "runs" || parsed.command === "show-run") { if (!fs.existsSync(PROJECT_PIPELINE_STATUS_SCRIPT)) return { ok: false, summary: "project-pipeline status helper missing; run /pidex-init-home or update the canonical PIDEX runtime" }; const args = [PROJECT_PIPELINE_STATUS_SCRIPT, "--pidex-root", PACKAGE_ROOT, "--json"]; if (parsed.projectId) args.push("--project-id", parsed.projectId); const proc = spawnSync(process.execPath, args, { cwd: PACKAGE_ROOT, encoding: "utf8", timeout: 30_000, maxBuffer: 5 * 1024 * 1024 }); try { const json = JSON.parse(proc.stdout || "{}"); if (parsed.command === "diagnose") return { ok: proc.status === 0 && json.ok !== false, summary: summarizeProjectDiagnose((json.projects || [])[0] || { project_id: parsed.projectId, runs: [] }) }; if (parsed.command === "runs") return { ok: proc.status === 0 && json.ok !== false, summary: summarizeProjectRuns((json.projects || [])[0] || { project_id: parsed.projectId, runs: [] }) }; if (parsed.command === "show-run") return { ok: proc.status === 0 && json.ok !== false, summary: summarizeProjectRun((json.projects || [])[0] || { project_id: parsed.projectId, runs: [] }, parsed.runId) }; return { ok: proc.status === 0 && json.ok !== false, summary: summarizeProjectRecords(json.projects || []) }; } catch { return { ok: false, summary: `project-pipeline ${parsed.command} failed exit=${proc.status}; helper output omitted to avoid metadata exposure` }; } } if (parsed.command === "preview") { if (!fs.existsSync(PROJECT_PIPELINE_PREVIEW_SCRIPT)) return { ok: false, summary: "project-pipeline preview helper missing; run /pidex-init-home or update the canonical PIDEX runtime" }; const args = [PROJECT_PIPELINE_PREVIEW_SCRIPT, parsed.action, "--pidex-root", PACKAGE_ROOT, "--project-id", parsed.projectId, "--json"]; if (parsed.action === "start") args.push("--", ...(parsed.commandArgs || [])); const proc = spawnSync(process.execPath, args, { cwd: PACKAGE_ROOT, encoding: "utf8", timeout: parsed.action === "start" ? 120_000 : 30_000, maxBuffer: 5 * 1024 * 1024 }); try { const json = JSON.parse(proc.stdout || "{}"); if (parsed.action === "start") return { ok: proc.status === 0 && json.ok === true, no_fallback: true, summary: summarizePreviewStart(json) }; return { ok: proc.status === 0 && json.ok !== false, no_fallback: true, summary: summarizePreviewStatusLike(parsed.action, json, parsed.projectId) }; } catch { return { ok: false, no_fallback: true, summary: `project-pipeline preview ${parsed.action} failed exit=${proc.status}; helper output omitted to avoid metadata exposure` }; } } if (parsed.command === "credentials") { if (!fs.existsSync(PROJECT_PIPELINE_CREDENTIALS_SCRIPT)) return { ok: false, summary: `project-pipeline credentials helper missing at ${PROJECT_PIPELINE_CREDENTIALS_SCRIPT}` }; const args = [PROJECT_PIPELINE_CREDENTIALS_SCRIPT, parsed.action, "--pidex-root", PACKAGE_ROOT, "--project-id", parsed.projectId, "--json"]; const proc = spawnSync(process.execPath, args, { cwd: PACKAGE_ROOT, encoding: "utf8", timeout: 60_000, maxBuffer: 5 * 1024 * 1024 }); try { const json = JSON.parse(proc.stdout || "{}"); if (parsed.action === "status") { if (proc.status !== 0 || json.ok === false) return { ok: false, summary: "Project Pipeline credentials status failed; helper details omitted to avoid credential metadata exposure" }; return { ok: true, summary: summarizeProjectCredentials(json.project_id ?? parsed.projectId, json.credentials ?? {}) }; } return { ok: proc.status === 0 && json.ok !== false, summary: json.ok === true ? `Project Pipeline credentials reset: ${json.project_id ?? parsed.projectId}` : "Project Pipeline credentials reset failed; helper details omitted to avoid credential metadata exposure" }; } catch { return { ok: false, summary: `project-pipeline credentials ${parsed.action} failed exit=${proc.status}; helper output omitted to avoid credential metadata exposure` }; } } if (parsed.command === "upgrade-pi") { if (parsed.confirm !== parsed.projectId) return { ok: false, summary: "Project Pipeline upgrade-pi requires exact project confirmation" }; const script = path.join(path.dirname(PROJECT_PIPELINE_LIFECYCLE_SCRIPT), "upgrade-pi.mjs"); if (!fs.existsSync(script)) return { ok: false, summary: "Project Pipeline upgrade-pi helper missing; update the canonical PIDEX runtime" }; const proc = spawnSync(process.execPath, [script, "--pidex-root", PACKAGE_ROOT, "--project-id", parsed.projectId, "--confirm", parsed.confirm, "--json"], { cwd: PACKAGE_ROOT, encoding: "utf8", timeout: 300_000, maxBuffer: 1024 * 1024 }); try { const json = JSON.parse(proc.stdout || "{}"); const ok = proc.status === 0 && json.ok === true && json.status === "verified" && /^\d+\.\d+\.\d+$/.test(json.after ?? "") && json.after === json.target; const status = ["verified", "blocked", "held", "failed_unchanged", "rolled_back"].includes(json.status) ? json.status : "unconfirmed"; const reason = ["confirmation-required", "project-execution-busy", "pi-maintenance-held", "project-not-idle", "container-not-idle", "container-identity-mismatch", "container-mount-mismatch", "docker-unavailable", "version-probe-failed", "invalid-version-pin", "upgrade-unconfirmed", "unsupported-pi-layout", "downgrade-denied", "install-unconfirmed", "install-failed", "version-mismatch", "container-update-failed"].includes(json.error) ? json.error : "details-omitted"; return { ok, no_fallback: true, summary: ok ? `Project Pipeline Pi verified: ${json.after}. Container/image not replaced; pipeline not resumed.` : `Project Pipeline upgrade-pi ${status}: ${reason}; no pipeline continuation. ${Array.isArray(json.failed_checks) ? summarizeUpgradeIdentity(json) + ". " : ""}Inspect maintenance receipts locally; helper output omitted.` }; } catch { return { ok: false, no_fallback: true, summary: "Project Pipeline upgrade-pi unconfirmed; keep HOLD. Helper output omitted." }; } } if (!fs.existsSync(PROJECT_PIPELINE_LIFECYCLE_SCRIPT)) return { ok: false, summary: "project-pipeline lifecycle helper missing; run /pidex-init-home or update the canonical PIDEX runtime" }; const lifecycleArgs = parsed.command === "open" ? [PROJECT_PIPELINE_LIFECYCLE_SCRIPT, "open", "--pidex-root", PACKAGE_ROOT, "--project-id", parsed.projectId, "--json"] : parsed.command === "repair" ? [PROJECT_PIPELINE_LIFECYCLE_SCRIPT, "repair", "--pidex-root", PACKAGE_ROOT, "--project-id", parsed.projectId, "--confirm", parsed.confirm, "--json"] : [PROJECT_PIPELINE_LIFECYCLE_SCRIPT, "remove", "--pidex-root", PACKAGE_ROOT, "--project-id", parsed.projectId, "--confirm", parsed.confirm, "--json"]; const proc = spawnSync(process.execPath, lifecycleArgs, { cwd: PACKAGE_ROOT, encoding: "utf8", timeout: 120_000, maxBuffer: 5 * 1024 * 1024 }); try { const json = JSON.parse(proc.stdout || "{}"); if (parsed.command === "open") return { ok: proc.status === 0 && json.ok !== false, summary: json.ok === true ? `Project Pipeline sandbox opened: ${json.record?.project_id ?? parsed.projectId}` : "Project Pipeline open failed; helper details omitted" }; if (parsed.command === "repair") { const ok = proc.status === 0 && json.ok !== false; const projectId = json.record?.project_id ?? parsed.projectId; const action = json.action === "started-container" || json.action === "recreated-container" ? json.action : undefined; const missing = Array.isArray(json.missing_volumes) ? json.missing_volumes.filter((v: any) => ["workspace", "secrets", "cache"].includes(String(v))).join(",") : ""; return { ok, summary: ok ? `Project Pipeline sandbox repaired: ${projectId} action=${action ?? "unknown"}` : `Project Pipeline repair failed: ${json.reason === "missing-volumes" ? `missing_volumes=${missing || "unknown"}` : "helper details omitted"}` }; } return { ok: proc.status === 0 && json.ok !== false, summary: json.ok === true ? `Project Pipeline sandbox removed: ${json.project_id}` : "Project Pipeline remove failed; helper details omitted" }; } catch { return { ok: false, summary: `project-pipeline ${parsed.command} failed exit=${proc.status}; helper output omitted to avoid metadata exposure` }; } } export function projectPipelinePiCredentialsConfigured(projectRoot: string, options: { script?: string; runner?: typeof spawnSync } = {}): boolean { const script = options.script ?? PROJECT_PIPELINE_CREDENTIALS_SCRIPT; if (!fs.existsSync(script)) return false; const projectId = slugForProjectPipelineId(projectRoot); const runner = options.runner ?? spawnSync; const proc = runner(process.execPath, [script, "status", "--pidex-root", PACKAGE_ROOT, "--project-id", projectId, "--json"], { cwd: PACKAGE_ROOT, encoding: "utf8", timeout: 30_000, maxBuffer: 1024 * 1024 }); if (proc.status !== 0) return false; try { const result = JSON.parse(String(proc.stdout || "{}")); return result.ok === true && result.credentials?.pi === "configured"; } catch { return false; } } export async function maybeCopyProjectPipelinePiCredentials(ctx: any, projectRoot: string, options: { credentialsConfigured?: (projectRoot: string) => boolean } = {}): Promise { const unattendedCredentialChoice = process.env.PIDEX_PROJECT_PIPELINE_COPY_PI_CREDENTIALS; if (unattendedCredentialChoice === "1") return true; if (unattendedCredentialChoice === "0") return false; const credentialsConfigured = options.credentialsConfigured ?? projectPipelinePiCredentialsConfigured; if (credentialsConfigured(projectRoot)) return false; if (!ctx.hasUI) return undefined; const choice = await ctx.ui.select("Project Pipeline runs Pi inside the persistent Project Sandbox. Pi credentials are not configured for this project. Copy host Pi auth/settings into its trusted per-project secrets volume?", ["Copy Pi credentials", "Skip credentials", "Cancel"]); if (choice === "Cancel") return undefined; return choice === "Copy Pi credentials"; } export function shouldStartProjectPipelineRunFlow(result: ProjectPipelineModeResult): boolean { return result.ok === true && result.mode === "project-pipeline"; } async function startProjectPipelineRunFlow(ctx: any, task: string | undefined): Promise { const initialTask = task?.trim(); if (!initialTask) { await ctx.ui.notify("Project Pipeline mode is fail-closed and the direct orchestrator bridge requires an initial task. Re-run /pd with a task description; no host-direct fallback was used.", "warning"); return; } const projectRoot = ctx.cwd ?? process.cwd(); const copyPiCredentials = await maybeCopyProjectPipelinePiCredentials(ctx, projectRoot); if (copyPiCredentials === undefined) { await ctx.ui.notify("Project Pipeline orchestrator cancelled before credential decision; no fallback was used.", "warning"); return; } await ctx.ui.notify("Starting Project Pipeline orchestrator in persistent Docker Project Sandbox. Progress will appear here while Docker/Pi child agents run; press Ctrl+C to interrupt.", "info"); const result = await runProjectPipelineRunFlowAsync({ projectRoot, task: initialTask, copyPiCredentials, acknowledgeTrustedPersistentContainer: copyPiCredentials }, { onProgress: (message) => { void ctx.ui.notify(`Project Pipeline: ${message}`, "info"); }, }); if (!result.ok) { await ctx.ui.notify(`Project Pipeline orchestrator failed closed (no fallback). ${result.error ?? `exit=${result.exitCode}`}`, "error"); return; } await ctx.ui.notify(summarizeProjectPipelineRunFlowResult(result), "info"); if (result.projectId && isProjectPipelineUiPreviewTask(initialTask)) { const preview = startDefaultProjectPipelinePreview(result.projectId); await ctx.ui.notify(preview.ok ? `Project Pipeline managed preview started for G9/user verification.\n\n${preview.summary}\n\nPlease inspect the URL and reply approve/reject.` : `Project Pipeline managed preview did not start automatically. ${preview.summary}\n\nNo source fallback was used; use logs/status or retry preview after fixing the app/server.`, preview.ok ? "info" : "warning"); } } function sandboxEvidenceLine(state: SandboxState = resolveSandboxState()): string { return state.enabled ? `sandbox_mode: hardened-pipeline; sandbox_reason: ${state.reason}; image: ${state.profile?.image ?? "node:22-slim"}` : `sandbox_mode: off; sandbox_reason: ${state.reason}`; } function probeSandboxAvailability(): { ok: boolean; summary: string } { const script = path.join(PACKAGE_ROOT, "modules", "pidex", "sandbox-runtime", "scripts", "sandbox", "probe.mjs"); if (!fs.existsSync(script)) return { ok: false, summary: `sandbox runtime missing at ${script}. Sandbox requires the canonical ~/pidex runtime checkout with initiative 021 files; run /pidex-init-home or update ~/pidex.` }; const proc = spawnSync(process.execPath, [script, "--json"], { cwd: PACKAGE_ROOT, encoding: "utf8", timeout: 180_000 }); const raw = `${proc.stdout || ""}\n${proc.stderr || ""}`.trim(); try { const parsed = JSON.parse(proc.stdout || "{}"); const ok = parsed?.ok === true && parsed?.available === true; return { ok, summary: ok ? `probe ok (${parsed.os}, ${parsed.image})` : `probe unavailable: ${parsed.reason ?? "unknown"}; ${parsed.actionable ?? raw}` }; } catch { return { ok: false, summary: `probe failed exit=${proc.status}: ${clipEnd(raw, 1200)}` }; } } function runSandboxJson(scriptName: string, args: string[], timeoutMs = 300_000): any { const script = path.join(PACKAGE_ROOT, "modules", "pidex", "sandbox-runtime", "scripts", "sandbox", scriptName); const proc = spawnSync(process.execPath, [script, ...args], { cwd: PACKAGE_ROOT, encoding: "utf8", timeout: timeoutMs, maxBuffer: 50 * 1024 * 1024 }); const raw = `${proc.stdout || ""}\n${proc.stderr || ""}`.trim(); let parsed: any; try { const start = (proc.stdout || "").indexOf("{"); const end = (proc.stdout || "").lastIndexOf("}"); parsed = start >= 0 && end >= start ? JSON.parse((proc.stdout || "").slice(start, end + 1)) : undefined; } catch {} if (proc.status !== 0) { const detail = parsed ? JSON.stringify(parsed) : clipEnd(raw, 2000); const error = new Error(`sandbox ${scriptName} failed exit=${proc.status}: ${detail}`) as Error & { sandboxResult?: any }; if (parsed) error.sandboxResult = parsed; throw error; } if (!parsed) throw new Error(`sandbox ${scriptName} failed exit=${proc.status}: no JSON result; ${clipEnd(raw, 2000)}`); if (parsed.ok === false) { const error = new Error(`sandbox ${scriptName} returned ok=false: ${JSON.stringify(parsed)}`) as Error & { sandboxResult?: any }; error.sandboxResult = parsed; throw error; } return parsed; } const SANDBOX_RUNTIME_CLEAN_PREFIXES = ["agents.output/", "pidex/state/", "pidex/context/", "state/", "logs/", ".fallow/"]; const SANDBOX_ALLOWED_GITIGNORE_ADDITIONS = new Set(["agents.output/", "pidex/state/", ".fallow/", ".wiki-migration/"]); function normalizeSandboxRel(relPath: string | undefined): string { return String(relPath || "").replaceAll("\\", "/").replace(/^\.\//, ""); } function gitPathIgnored(cwd: string, relPath: string): boolean { const proc = spawnSync("git", ["check-ignore", "-q", "--", relPath], { cwd, encoding: "utf8", timeout: 5000 }); return proc.status === 0; } function gitignoreRuntimeOnlyChange(cwd: string): { ok: boolean; reason: string; added?: string[]; entry?: string } { const unstaged = spawnSync("git", ["diff", "--", ".gitignore"], { cwd, encoding: "utf8", timeout: 5000 }); const staged = spawnSync("git", ["diff", "--cached", "--", ".gitignore"], { cwd, encoding: "utf8", timeout: 5000 }); if (unstaged.status !== 0 || staged.status !== 0) return { ok: false, reason: "git-diff-failed" }; const added: string[] = []; for (const line of `${unstaged.stdout || ""}\n${staged.stdout || ""}`.split(/\r?\n/)) { if (!line || line.startsWith("diff --git ") || line.startsWith("index ") || line.startsWith("@@ ") || line.startsWith("--- ") || line.startsWith("+++ ")) continue; if (line.startsWith("-")) return { ok: false, reason: "gitignore-deletion-or-rewrite" }; if (line.startsWith("+")) { const entry = line.slice(1).trim(); if (!SANDBOX_ALLOWED_GITIGNORE_ADDITIONS.has(entry)) return { ok: false, reason: "gitignore-unapproved-addition", entry }; added.push(entry); } } return { ok: added.length > 0, reason: added.length ? "allowed-runtime-gitignore-additions" : "no-gitignore-diff", added }; } function gitPathTracked(cwd: string, relPath: string): boolean { const proc = spawnSync("git", ["ls-files", "--error-unmatch", "--", relPath], { cwd, encoding: "utf8", timeout: 5000 }); return proc.status === 0; } function isSandboxCleanStatusPath(cwd: string, relPath: string | undefined): boolean { const normalized = normalizeSandboxRel(relPath); if (SANDBOX_RUNTIME_CLEAN_PREFIXES.some((prefix) => normalized.startsWith(prefix))) return true; if (normalized.startsWith("wiki/")) return gitPathIgnored(cwd, normalized); if (normalized === ".gitignore") return gitignoreRuntimeOnlyChange(cwd).ok; return false; } function isValidationOnlyGeneratedPath(cwd: string, relPath: string | undefined): boolean { const normalized = normalizeSandboxRel(relPath); if (isSandboxCleanStatusPath(cwd, normalized)) return true; return normalized.startsWith("wiki/") && !gitPathTracked(cwd, normalized); } export function validationSourceMutationFiles(cwd: string, changedFiles: any): string[] { if (!Array.isArray(changedFiles)) return []; const out: string[] = []; for (const entry of changedFiles) { for (const rel of Array.isArray(entry?.paths) ? entry.paths : []) { if (!isValidationOnlyGeneratedPath(cwd, rel)) out.push(normalizeSandboxRel(rel)); } } return [...new Set(out)].sort(); } export function gitSourceStatusPorcelain(cwd: string): string { const proc = spawnSync("git", ["status", "--porcelain", "--untracked-files=all"], { cwd, encoding: "utf8", timeout: 10_000 }); if (proc.status !== 0) return ""; return proc.stdout.trim().split(/\r?\n/).filter(Boolean).filter((line) => { const rel = line.slice(2).trim().split(" -> ").pop(); return !isSandboxCleanStatusPath(cwd, rel); }).join("\n"); } function routingContextFile(text: string): string | undefined { const block = extractRoutingBlock(text); const value = extractRoutingField(block, "context_file"); return value || undefined; } function normalizeContextRel(value: string | undefined): string | undefined { if (!value) return undefined; return value.replaceAll("\\", "/").replace(/^\.\//, ""); } export function validateSandboxRoutingContext(agent: string, contextRel: string | undefined): { ok: boolean; reason?: string } { if (contextRel?.startsWith("agents.output/")) return { ok: true }; return { ok: false, reason: `PIDEX sandbox requires ${agent} to return ROUTING context_file under agents.output/** so reports use the artifact channel, not source files. got=${contextRel || "(missing)"}` }; } function copyValidationContextArtifacts(projectRoot: string, workspace: string): { copied: boolean; source?: string; destination?: string } { const source = path.join(projectRoot, "agents.output"); if (!fs.existsSync(source)) return { copied: false }; const destination = path.join(workspace, "agents.output"); fs.mkdirSync(path.dirname(destination), { recursive: true }); fs.cpSync(source, destination, { recursive: true, force: true, errorOnExist: false }); return { copied: true, source, destination }; } async function runSandboxedConfiguredAgent(params: { agent: string; task: string; cwd: string; route?: HostAgentRoute; providerOverride?: string; modelOverride?: string; effortOverride?: string; tools?: string[]; signal?: AbortSignal; onUpdate?: (text: string) => void; }): Promise { const sandboxPurpose = params.agent === "pidex-qa" || params.agent === "pidex-security" ? "validation" : "mutation"; const sourceStatus = gitSourceStatusPorcelain(params.cwd); if (sandboxPurpose === "mutation" && sourceStatus) throw new Error(`PIDEX sandbox requires a clean host source worktree before sandbox creation; runtime artifacts under agents.output/, pidex/state/, pidex/context/, .fallow/, and allowed .gitignore runtime entries are ignored. Dirty source status:\n${sourceStatus}`); const create = runSandboxJson("lifecycle.mjs", ["--project", params.cwd, "--pidex-root", PACKAGE_ROOT, "--mode", "hardened-pipeline", "--json"]); const workspace = String(create.workspace || ""); const validationContextCopy = sandboxPurpose === "validation" ? copyValidationContextArtifacts(params.cwd, workspace) : { copied: false }; const metadataPath = String(create.metadata_path || ""); const metadata = metadataPath && fs.existsSync(metadataPath) ? JSON.parse(fs.readFileSync(metadataPath, "utf8")) : create.metadata; const baselineHead = metadata?.baseline_head; const runId = String(create.run_id || metadata?.run_id || ""); const task = [ `SANDBOX RUN: ${runId}`, `HOST_PROJECT_ROOT: ${params.cwd}`, `SANDBOX_WORKSPACE: ${workspace}`, `SANDBOX_PURPOSE: ${sandboxPurpose}`, "Write/edit only inside SANDBOX_WORKSPACE. Keep normal ROUTING context_file paths project-relative under agents.output/....", "Do not run raw host bash or host git commands. Do not attempt git commit/status as a completion gate. The PIDEX sandbox wrapper will generate/apply the source patch for implementer runs and extract your agents.output artifact after your final ROUTING response.", `Allowed validation helper shape: node ${path.join(PACKAGE_ROOT, "modules", "pidex", "sandbox-runtime", "scripts", "sandbox", "exec.mjs")} --project ${workspace} --pidex-root ${PACKAGE_ROOT} --mode hardened-pipeline --phase test --json -- npm test`, params.task, ].join("\n\n"); try { const sandboxContext: SandboxRuntimeContext = { mode: "hardened-pipeline", runId, hostProjectRoot: path.resolve(params.cwd), sandboxWorkspace: path.resolve(workspace), allowedWriteRoot: path.resolve(workspace), }; const result = await runConfiguredAgent({ ...params, task, cwd: workspace, sandboxContext }); const contextRel = normalizeContextRel(routingContextFile(result.finalText)); const routingValidation = validateSandboxRoutingContext(params.agent, contextRel); if (!routingValidation.ok) throw new Error(routingValidation.reason); runSandboxJson("extract-artifacts.mjs", ["--workspace", workspace, "--project", params.cwd, "--assigned", contextRel!, "--run-id", runId, "--check", "--json"]); const patches = path.join(PACKAGE_ROOT, "state", "sandbox", "runs", runId, "patches"); const diff = runSandboxJson("diff.mjs", ["--workspace", workspace, "--patches", patches, "--json"]); if (sandboxPurpose === "validation") { runSandboxJson("extract-artifacts.mjs", ["--workspace", workspace, "--project", params.cwd, "--assigned", contextRel!, "--run-id", runId, "--json"]); const validationMutations = validationSourceMutationFiles(params.cwd, diff.changed_files); if (validationMutations.length) { throw new Error(`validation-agent-source-mutation: ${params.agent} changed source files in validation mode; changed=${JSON.stringify(validationMutations)}`); } runSandboxJson("cleanup.mjs", ["--pidex-root", PACKAGE_ROOT, "--run-id", runId, "--success", "--json"], 120_000); return { ...result, warnings: [...(result.warnings ?? []), `SANDBOX: run_id=${runId}; workspace=${workspace}; validation_source_diff=${diff.empty ? "empty" : "runtime-only"}; context_artifacts=${validationContextCopy.copied ? "copied" : "absent"}; artifact=extracted:${contextRel}`], }; } if (!diff.empty) { runSandboxJson("apply.mjs", ["--project", params.cwd, "--patch", diff.patch_path, "--changed-files", diff.changed_files_path, "--baseline-head", baselineHead, "--json"]); } runSandboxJson("extract-artifacts.mjs", ["--workspace", workspace, "--project", params.cwd, "--assigned", contextRel!, "--run-id", runId, "--json"]); runSandboxJson("cleanup.mjs", ["--pidex-root", PACKAGE_ROOT, "--run-id", runId, "--success", "--json"], 120_000); return { ...result, warnings: [...(result.warnings ?? []), `SANDBOX: run_id=${runId}; workspace=${workspace}; source_patch=${diff.empty ? "empty" : "applied"}; artifact=extracted:${contextRel}`], }; } catch (error: any) { let cleanupNote = ""; try { const cleanup = runSandboxJson("cleanup.mjs", ["--pidex-root", PACKAGE_ROOT, "--run-id", runId, "--preserve-on-failure", "--json"], 120_000); if (cleanup?.ok === false) cleanupNote = `; cleanup_failed=${JSON.stringify(cleanup)}`; } catch (cleanupError: any) { cleanupNote = `; cleanup_error=${cleanupError?.message ?? cleanupError}`; } throw new Error(`Sandboxed pidex_agent failed for ${params.agent}; run_id=${runId}; workspace=${workspace}; reason=${error?.message ?? error}${cleanupNote}`); } } function normalizeProvider(provider: string | undefined): string { return (provider ?? "pi").trim().toLowerCase(); } function isPiProvider(provider: string): boolean { return provider === "pi" || provider === "subagent" || provider === "pidex_agent"; } function supportsDelegateToolLoop(provider: string): boolean { return provider === "codex"; } function isToolHeavyAgent(agent: string): boolean { return TOOL_HEAVY_AGENTS.has(agent); } function formatAgentProgressLabel(agent: string): string { const role = agent .replace(/^pidex-/, "") .split("-") .map((part) => part ? part[0].toUpperCase() + part.slice(1) : part) .join(" "); return role || agent; } export function formatPiRunnerStartDetails(model: string | undefined, effort?: string): string { const trimmed = model?.trim(); const thinking = effort?.trim() || "default"; if (!trimmed) return `auto auto ${thinking}`; const slash = trimmed.indexOf("/"); if (slash > 0 && slash < trimmed.length - 1) { return `${trimmed.slice(0, slash)} ${trimmed.slice(slash + 1)} ${thinking}`; } return `auto ${trimmed} ${thinking}`; } export function formatDelegateStartDetails(provider: string, model: string | undefined, effort?: string): string { return `${provider} ${model?.trim() || "default"} ${effort?.trim() || "default"}`; } export function formatConfiguredRouteStartDetails(routeSource: string, provider: string, model?: string, effort?: string): string { return `${routeSource} → ${provider} / ${model?.trim() || "default"} / ${effort?.trim() || "default"}`; } function formatAgentCompletionLine(result: RpResult): string { const runner = result.provider || "unknown"; const modelForDisplay = result.modelRequested ?? result.model; const details = runner === "pi" ? formatPiRunnerStartDetails(modelForDisplay, result.effort) : formatDelegateStartDetails(runner, modelForDisplay, result.effort); return `${formatAgentProgressLabel(result.agent)}: ${details}`; } function hasRoutingBlock(text: string): boolean { return //.test(text); } function extractRoutingBlock(text: string): string | undefined { const matches = text.match(//g); return matches?.[matches.length - 1]; } function extractRoutingField(routingBlock: string | undefined, field: string): string | undefined { if (!routingBlock) return undefined; const match = routingBlock.match(new RegExp(`(?:^|\\n)\\s*${field}:\\s*(.+)`, "i")); return match?.[1]?.trim(); } function hasValidRoutingContextFile(text: string, cwd: string): boolean { const routing = extractRoutingBlock(text); const contextFile = extractRoutingField(routing, "context_file"); if (!contextFile) return false; const normalized = contextFile.trim(); if (!normalized) return false; const resolved = path.isAbsolute(normalized) ? normalized : path.resolve(cwd, normalized); return fs.existsSync(resolved); } function loadAgent(agentName: string): { frontmatter: AgentFrontmatter; body: string; filePath: string } { const safeName = agentName.endsWith(".md") ? agentName : `${agentName}.md`; if (!/^pidex-[a-z0-9-]+\.md$/.test(safeName)) throw new Error(`Invalid pidex agent name: ${agentName}`); const filePath = path.join(AGENTS_DIR, safeName); if (!fs.existsSync(filePath)) { const available = fs .readdirSync(AGENTS_DIR) .filter((f) => f.startsWith("pidex-") && f.endsWith(".md")) .map((f) => f.slice(0, -3)) .sort() .join(", "); throw new Error(`Unknown pidex agent: ${agentName}. Available: ${available}`); } const content = fs.readFileSync(filePath, "utf8"); const { frontmatter, body } = parseFrontmatter(content); return { frontmatter, body, filePath }; } function textFromAssistantMessage(message: any): string { if (message?.role !== "assistant") return ""; if (typeof message.content === "string") return message.content; if (Array.isArray(message.content)) { const texts = message.content.filter((p: any) => p?.type === "text" && typeof p.text === "string").map((p: any) => p.text); return texts.join("\n"); } return ""; } function finalAssistantTextFromEvent(event: JsonEvent): string { const direct = textFromAssistantMessage(event?.message); if (direct) return direct; if (Array.isArray(event?.messages)) { for (let i = event.messages.length - 1; i >= 0; i--) { const text = textFromAssistantMessage(event.messages[i]); if (text) return text; } } return ""; } async function writeTempPrompt(agent: string, prompt: string): Promise<{ dir: string; file: string }> { const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "pidex-agent-")); const file = path.join(dir, `${agent}.system.md`); await fs.promises.writeFile(file, prompt, { encoding: "utf8", mode: 0o600 }); return { dir, file }; } function buildAgentSystemPrompt(agentName: string, body: string, provider: string): string { return `${body}\n\n---\npidex adapter notes:\n- You are running as ${agentName} through provider '${provider}'.\n- You are a child pidex role agent, not the parent orchestrator. Complete only your assigned pidex-* role.\n- Do not start or invoke pidex recursively. Do not propose subagent fanout unless explicitly requested by the parent task.\n- Use pidex pidex-* names and conventions.\n- If you need user input, do not block; emit a ROUTING block with the appropriate gate and question for the orchestrator.\n- Write full artifacts to files under the requested agents.output/ or wiki path; do not paste full documents into your final response.\n- Final response must be <= ${MAX_AGENT_FINAL_CHARS} characters and contain only: status, output path(s), next agent/route, concise evidence, and the ROUTING HTML comment.\n- Always finish with exactly one ROUTING HTML comment that includes context_file, then stop immediately after it.\n`; } function buildCliDelegatePrompt(agentName: string, body: string, task: string, provider: string): string { return `${buildAgentSystemPrompt(agentName, body, provider)}\n\n---\nTask for ${agentName}:\n\n${task}\n`; } function appendTail(current: string, addition: string, maxChars = MAX_STDERR_CHARS): string { const combined = current + addition; return combined.length > maxChars ? combined.slice(-maxChars) : combined; } function clipEnd(text: string, maxChars: number): string { if (text.length <= maxChars) return text; return `[truncated: showing last ${maxChars} of ${text.length} chars]\n${text.slice(-maxChars)}`; } function truncateToolContent(text: string, result: RpResult): string { if (text.length <= MAX_TOOL_CONTENT_CHARS) return text; const routing = extractRoutingBlock(text); const head = text.slice(0, 6000); const tail = text.slice(-6000); const routingSection = routing && !head.includes(routing) && !tail.includes(routing) ? `\n\n${routing}` : ""; const logHint = result.logFile ? `\nFull raw child log: ${result.logFile}` : ""; return `${head}\n\n[pidex truncated final response from ${text.length} chars to avoid session bloat.${logHint}]\n\n${tail}${routingSection}`; } function safeDetailsForResult(result: RpResult): Record { const details: Record = { agent: result.agent, provider: result.provider, model: result.model, modelRequested: result.modelRequested, effort: result.effort, exitCode: result.exitCode, fallbackFrom: result.fallbackFrom, logFile: result.logFile, runDir: result.runDir, sessionDir: result.sessionDir, stdoutEventCount: result.stdoutEventCount, skippedLargeEventCount: result.skippedLargeEventCount, toolCount: result.toolCount, turnCount: result.turnCount, maxTurns: result.maxTurns, durationMs: result.durationMs, inputChars: result.inputChars, outputChars: result.outputChars ?? result.finalText.length, finalTextChars: result.finalText.length, inputTokensEstimate: result.inputTokensEstimate, outputTokensEstimate: result.outputTokensEstimate, cacheReadTokens: result.cacheReadTokens, cacheWriteTokens: result.cacheWriteTokens, costUsdEstimate: result.costUsdEstimate, metricsFile: result.metricsFile, setupError: result.setupError, timedOut: result.timedOut, aborted: result.aborted, turnLimitHit: result.turnLimitHit, warnings: result.warnings, stderrTail: result.stderr ? clipEnd(result.stderr, MAX_STDERR_DETAILS_CHARS) : undefined, }; if (JSON.stringify(details).length <= MAX_TOOL_DETAILS_CHARS) return details; return { agent: result.agent, provider: result.provider, model: result.model, modelRequested: result.modelRequested, exitCode: result.exitCode, logFile: result.logFile, finalTextChars: result.finalText.length, stderrTail: result.stderr ? clipEnd(result.stderr, 2048) : undefined, warnings: [...(result.warnings ?? []), "Tool details were compacted because they exceeded the size guard."], }; } function formatToolContent(result: RpResult): string { const base = result.finalText || `(no final assistant text; exit ${result.exitCode})`; let text = `${formatAgentCompletionLine(result)}\n\n${truncateToolContent(base, result)}`; if (!result.finalText && result.stderr) { text += `\n\nstderr tail:\n${clipEnd(result.stderr, 4000)}`; } return text; } function createThrottledUpdate(onUpdate?: (text: string) => void): (text: string, force?: boolean) => void { let lastText = ""; let lastAt = 0; return (text: string, force = false) => { if (!onUpdate) return; const clipped = clipEnd(text, MAX_UPDATE_CHARS); if (clipped === lastText) return; const now = Date.now(); if (!force && now - lastAt < UPDATE_INTERVAL_MS) return; lastText = clipped; lastAt = now; onUpdate(clipped); }; } function safePathSegment(value: string): string { const segment = value.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, ""); return segment.slice(0, 80) || "unknown"; } function timestampForPath(date = new Date()): string { return date.toISOString().replace(/[:.]/g, "-"); } type AgentRunLog = { runId: string; runDir: string; logFile: string; sessionDir: string; stderrFile: string; writeChunk(chunk: Buffer | string): boolean; onDrain(callback: () => void): void; close(): Promise; }; function createAgentRunLog(agent: string, cwd: string): AgentRunLog { const runId = `${timestampForPath()}_${safePathSegment(agent)}_${process.pid}_${randomUUID().slice(0, 8)}`; const project = safePathSegment(cwd); const dir = path.join(RUNS_DIR, project, runId); fs.mkdirSync(dir, { recursive: true }); const logFile = path.join(dir, `${safePathSegment(agent)}.stdout.jsonl.gz`); const sessionDir = path.join(dir, "session"); const stderrFile = path.join(dir, "stderr.log"); const output = fs.createWriteStream(logFile, { mode: 0o600 }); const gzip = createGzip(); gzip.pipe(output); let closed = false; const closedPromise = new Promise((resolve, reject) => { output.on("close", resolve); output.on("error", reject); gzip.on("error", reject); }); return { runId, runDir: dir, logFile, sessionDir, stderrFile, writeChunk(chunk: Buffer | string) { if (closed) return true; return gzip.write(chunk); }, onDrain(callback: () => void) { gzip.once("drain", callback); }, async close() { if (closed) return; closed = true; gzip.end(); await closedPromise; }, }; } function estimateTokensFromChars(chars: number): number { return Math.max(0, Math.ceil(chars / 4)); } function normalizeModelForPricing(model?: string): string | undefined { if (!model) return undefined; const trimmed = model.trim(); if (!trimmed) return undefined; if (trimmed.startsWith("-m ")) return trimmed.slice(3).trim(); if (trimmed.startsWith("--model ")) return trimmed.slice(8).trim(); return trimmed; } function loadPricing(): Record { try { if (!fs.existsSync(PRICING_PATH)) return {}; return JSON.parse(fs.readFileSync(PRICING_PATH, "utf8")); } catch { return {}; } } function estimateCostUsd(model: string | undefined, inputTokens: number, outputTokens: number): number | undefined { const pricing = loadPricing(); const normalized = normalizeModelForPricing(model); if (!normalized) return undefined; const aliases: Record = { opus: "claude-opus", sonnet: "claude-sonnet", haiku: "claude-haiku", }; const key = pricing[normalized] ? normalized : aliases[normalized] ?? normalized; const price = pricing[key]; if (!price) return undefined; return (inputTokens / 1_000_000) * price.input + (outputTokens / 1_000_000) * price.output; } function isAnthropicModel(model: string | undefined): boolean { if (!model) return false; const normalized = model.trim().toLowerCase(); return normalized.startsWith("anthropic/") || normalized.startsWith("claude") || normalized.includes("/claude") || normalized === "opus" || normalized === "sonnet" || normalized === "haiku"; } function isCodexModel(model: string | undefined): boolean { if (!model) return false; const normalized = model.trim().toLowerCase(); return normalized.includes("codex") || normalized.startsWith("openai-codex/"); } function codexQuotaProviderFromModel(model: string | undefined): string { if (!model) return "codex"; const normalized = model.trim().toLowerCase(); if (normalized.includes("codex-spark")) return "codex-spark"; return "codex"; } function providerLimitRecord(provider: string, window: string): Record | undefined { try { if (!fs.existsSync(PROVIDER_LIMITS_LATEST_PATH)) return undefined; const data = JSON.parse(fs.readFileSync(PROVIDER_LIMITS_LATEST_PATH, "utf8")); return (data.records ?? []).find((r: any) => r?.provider === provider && r?.window === window); } catch { return undefined; } } function assertCodexQuotaAllowed(model: string | undefined): void { if (!isCodexModel(model)) return; // Quota enforcement is intentionally disabled in this local branch to allow uninterrupted Codex/PIDEX execution. return; } function assertPiModelAllowed(model: string | undefined): void { if (isAnthropicModel(model)) { if (process.env.PIDEX_ALLOW_ANTHROPIC === "1") return; throw new Error(`Blocked Pi runner model '${model}'. Anthropic/Claude subscription auth may incur paid extra usage; set PIDEX_ALLOW_ANTHROPIC=1 only for an explicit one-off override.`); } assertCodexQuotaAllowed(model); } function normalizeExtractedPlan(raw: string | undefined): string | undefined { if (!raw) return undefined; const cleaned = raw.trim().replace(/^plan[-_\s]*/i, "").replace(/\.jsonl$/i, ""); if (!/^[0-9][a-zA-Z0-9._-]{0,39}$/.test(cleaned)) return undefined; return `plan-${cleaned.toLowerCase()}`; } function extractPlanFromContextFile(contextFile: string | undefined): string | undefined { if (!contextFile) return undefined; const base = path.basename(contextFile.trim()); return normalizeExtractedPlan(base.match(/^([0-9][a-zA-Z0-9._-]*?)(?:[-_][A-Za-z]|\.md$|$)/)?.[1]); } function extractPlanId(task: string, finalText = ""): string { const combined = `${task}\n${finalText}`; const routing = extractRoutingBlock(finalText); const contextFile = extractRoutingField(routing, "context_file"); return normalizeExtractedPlan(combined.match(/\bplan\s*[=:]\s*([0-9][a-zA-Z0-9._-]*)\b/i)?.[1]) ?? normalizeExtractedPlan(combined.match(/\bPlan\s+([0-9][a-zA-Z0-9._-]*)\b/i)?.[1]) ?? normalizeExtractedPlan(combined.match(/\bID:\s*([0-9][a-zA-Z0-9._-]*)\b/i)?.[1]) ?? normalizeExtractedPlan(combined.match(/\bPlanID:\s*([0-9][a-zA-Z0-9._-]*)\b/i)?.[1]) ?? normalizeExtractedPlan(combined.match(/\bOrigin:\s*([0-9][a-zA-Z0-9._-]*)\b/i)?.[1]) ?? normalizeExtractedPlan(combined.match(/\bplan[-_]([0-9][a-zA-Z0-9._-]*)\b/i)?.[1]) ?? extractPlanFromContextFile(contextFile) ?? extractPlanFromContextFile(combined.match(/agents\.output\/[A-Za-z0-9_./-]*?[0-9][A-Za-z0-9._-]*[-_][A-Za-z0-9_.-]+\.md/)?.[0]) ?? "unknown-plan"; } function extractPlanUuid(task: string, finalText = ""): string | undefined { const combined = `${task}\n${finalText}`; return combined.match(/\bUUID:\s*([a-zA-Z0-9-]+)/)?.[1] ?? combined.match(/\buuid[=:]\s*([a-zA-Z0-9-]+)/i)?.[1]; } function normalizePlanKey(value: string | undefined): string { const raw = (value ?? "unknown-plan").trim() || "unknown-plan"; return normalizeReviewPlan(raw) ?? raw; } function operatorEventFile(cwd: string, planId: string): { file: string; pipelineId: string } { const project = safePathSegment(cwd); const normalizedPlan = normalizePlanKey(planId); const pipelineId = process.env.RUNNING_PI_PIPELINE_ID || process.env.PIDEX_PIPELINE_ID || `${project}-${safePathSegment(normalizedPlan)}`; const file = path.join(STATE_DIR, "orchestrator-events", project, `${safePathSegment(pipelineId)}.jsonl`); return { file, pipelineId }; } function appendOperatorEvent(cwd: string, planId: string, event: Record): string | undefined { try { const normalizedPlan = normalizePlanKey(planId); const { file, pipelineId } = operatorEventFile(cwd, normalizedPlan); fs.mkdirSync(path.dirname(file), { recursive: true }); const mode = projectModeForTelemetry(cwd); const record = { timestamp: new Date().toISOString(), project_path: cwd, project_slug: path.basename(cwd) || safePathSegment(cwd), pipeline_id: pipelineId, plan_key: normalizedPlan, project_mode: mode, actor: "orchestrator", source: "pidex_agent_extension", ...event, }; fs.appendFileSync(file, `${JSON.stringify(record)}\n`, { encoding: "utf8", mode: 0o600 }); return file; } catch { return undefined; } } function normalizeGate(value: string | undefined): string | undefined { const gate = (value || "").trim(); if (!gate || gate.toLowerCase() === "none" || gate === "—" || gate === "-") return undefined; return gate; } function estimateContextSizeClass(chars: number): "small" | "medium" | "large" { if (chars < 8_000) return "small"; if (chars < 32_000) return "medium"; return "large"; } function extractContextPathsFromTask(task: string): string[] { const matches = task.match(/(?:^|[\s`"'(<])((?:\.\/|\.\.\/|\/)?(?:agents\.output|rules|pidex\/context|wiki|scripts|config|templates|skills|extensions|dashboard)\/[A-Za-z0-9._/@:+-]+(?:\/[A-Za-z0-9._/@:+-]+)*\.(?:md|json|jsonl|ts|tsx|js|mjs|py|sh|yml|yaml|txt))/g) || []; const paths = matches .map((m) => m.trim().replace(/^[`"'(<]+|[`"'),>]+$/g, "")) .map((m) => m.replace(/^\.\//, "")); return Array.from(new Set(paths)).slice(0, 30); } function classifyInitialTask(task: string | undefined): string { const text = (task || "").toLowerCase(); if (!text.trim()) return "unknown"; if (/fix|bug|error|fail|broken|regression/.test(text)) return "bugfix"; if (/ui|ux|design|screen|page|dashboard|mobile/.test(text)) return "ui"; if (/release|publish|tag|push|deploy/.test(text)) return "release"; if (/refactor|cleanup|hygiene|remove|rename/.test(text)) return "cleanup"; if (/test|qa|coverage|playwright|smoke/.test(text)) return "qa"; return "feature"; } function recordPreflightSkeleton(cwd: string, task: string | undefined, authOk: boolean, gitHookStatus: string): string | undefined { const initialTask = task?.trim() || ""; return appendOperatorEvent(cwd, "unknown-plan", { operator_type: "OpPreflight", logical_decision: { initial_task_provided: Boolean(initialTask), task_class: classifyInitialTask(initialTask), existing_project: fs.existsSync(path.join(cwd, ".git")) || fs.existsSync(path.join(cwd, "package.json")) || fs.existsSync(path.join(cwd, "pidex", "context")), preflight_required: true, }, physical_action: { kickoff_sent: true, fixed_interview_required: true, grill_decision_pending: true, delegate_auth_ok: authOk, git_hook_status: gitHookStatus, }, confidence: "low", reason: "PIDEX direct-mode kickoff emitted preflight skeleton before interactive interview completion.", }); } export function admitReviewDispatch(agent: string, identity: unknown, folded: { status?: string } = {}): { allowed: boolean; code?: string } { const reviewers = new Set(["pidex-critic", "pidex-code-reviewer", "pidex-security", "pidex-qa"]); const review = validateReviewIdentity(identity); if (!review.ok) return reviewers.has(agent) ? { allowed: false, code: "REVIEW_IDENTITY_INVALID" } : { allowed: true }; const mode = (identity as any).reviewMode; const expected = mode.startsWith("correction") ? new Set([(identity as any).reviewGate === "critic" ? "pidex-planner" : "pidex-implementer"]) : new Set([{ critic: "pidex-critic", "code-review": "pidex-code-reviewer", security: "pidex-security", qa: "pidex-qa" }[(identity as any).reviewGate] as string]); if (!expected.has(agent) || !["allowed", "resume_reserved"].includes(String(folded.status))) return { allowed: false, code: "REVIEW_DISPATCH_DENIED" }; return { allowed: true }; } function isReviewAgent(agent: string): boolean { return new Set(["pidex-critic", "pidex-code-reviewer", "pidex-security", "pidex-qa", "pidex-uat"]).has(agent); } function extractFindingCounts(text: string): Record { const out: Record = {}; for (const severity of ["critical", "major", "minor", "high", "medium", "low"] as const) { const re = new RegExp(`\\b${severity}\\b`, "gi"); const count = text.match(re)?.length || 0; if (count) out[severity] = count; } return out; } function shouldSuppressAgentGateNotify(agent: string): boolean { if ((process.env.PIDEX_SUPPRESS_AGENT_GATE_NOTIFY || "0") === "1") return true; if ((process.env.PIDEX_PARALLEL_GATE_NOTIFY_AFTER_MERGE || "1") === "0") return false; // Critic/code-reviewer can have orchestrator-owned parallel lanes. Their primary // ROUTING block is not the final user-facing gate until secondary lanes have // returned and the orchestrator has written the merge/adjudication summary. return agent === "pidex-critic" || agent === "pidex-code-reviewer"; } function notifyGate(cwd: string, planId: string, gate: string, routeTo: string | undefined, contextFile: string | undefined, agent: string): void { if (shouldSuppressAgentGateNotify(agent)) return; try { if ((process.env.PIDEX_TELEGRAM_GATES || "1") === "0") return; const key = `${path.resolve(cwd)}|${normalizePlanKey(planId)}|${gate}|${contextFile || ""}`; const stateDir = path.join(PACKAGE_ROOT, "state", "telegram"); const stateFile = path.join(stateDir, "gate-notifications.json"); let state: Record = {}; try { state = JSON.parse(fs.readFileSync(stateFile, "utf8")); } catch { state = {}; } if (state[key]) return; const script = path.join(PACKAGE_ROOT, "scripts", "telegram", "notify.sh"); if (!fs.existsSync(script)) return; const context = [ `Gate: ${gate}`, `Plan: ${normalizePlanKey(planId)}`, routeTo ? `Route: ${routeTo}` : undefined, contextFile ? `Context: ${contextFile}` : undefined, "Action: return to the active Pi/PIDEX session and answer there.", ].filter(Boolean).join("\n"); const proc = spawnSync("bash", [script, "--optional", "--project", cwd, "--needs", `Gate ${gate} decision`, "--context", context], { cwd: PACKAGE_ROOT, encoding: "utf8", timeout: 15_000, stdio: ["ignore", "pipe", "pipe"], }); if (proc.status === 0) { fs.mkdirSync(stateDir, { recursive: true }); state[key] = new Date().toISOString(); fs.writeFileSync(stateFile, `${JSON.stringify(state, null, 2)}\n`, { encoding: "utf8", mode: 0o600 }); } } catch { // Gate notifications are best-effort and must never break pidex_agent. } } export function recordOperatorEvents(result: RpResult, cwd: string, task: string): string | undefined { const routing = extractRoutingBlock(result.finalText); const planId = extractPlanId(task, result.finalText); const contextFile = extractRoutingField(routing, "context_file"); const gate = normalizeGate(extractRoutingField(routing, "gate")); const routeTo = extractRoutingField(routing, "route_to"); const contextPaths = extractContextPathsFromTask(task); let eventFile = appendOperatorEvent(cwd, planId, { operator_type: "OpContextPack", agent: result.agent, run_dir: result.runDir, logical_decision: { agent: result.agent, task_chars: task.length, context_paths_detected: contextPaths.length }, physical_action: { context_paths: contextPaths, estimated_token_class: estimateContextSizeClass(task.length), budget_warning: task.length >= 32_000 }, confidence: contextPaths.length ? "medium" : "low", }); eventFile = appendOperatorEvent(cwd, planId, { operator_type: "OpSpawn", agent: result.agent, provider: result.provider, model: normalizeModelForPricing(result.model), duration_ms: result.durationMs, exit_code: result.exitCode, context_file: contextFile, log_file: result.logFile, run_dir: result.runDir, logical_decision: { agent: result.agent }, physical_action: { provider: result.provider, model: normalizeModelForPricing(result.model), exit_code: result.exitCode }, }); if (isReviewAgent(result.agent)) { eventFile = appendOperatorEvent(cwd, planId, { operator_type: "OpReview", agent: result.agent, run_dir: result.runDir, source_artifact: contextFile, logical_decision: { review_agent: result.agent, expected_verdict_in_routing: true }, physical_action: { verdict: extractRoutingField(routing, "verdict") || "unknown", gate: gate || undefined, route_to: routeTo, finding_counts: extractFindingCounts(result.finalText) }, confidence: routing ? "medium" : "low", }) ?? eventFile; } if (routeTo || gate) { eventFile = appendOperatorEvent(cwd, planId, { operator_type: "OpRoute", agent: result.agent, run_dir: result.runDir, source_artifact: contextFile, gate_present: Boolean(gate), logical_decision: { route_to: routeTo, gate: gate || undefined, context_file: contextFile }, physical_action: { returned_to_orchestrator: true }, confidence: routeTo ? "medium" : "insufficient-data", }) ?? eventFile; } if (gate) { eventFile = appendOperatorEvent(cwd, planId, { operator_type: "OpGate", agent: result.agent, run_dir: result.runDir, source_artifact: contextFile, gate, logical_decision: { gate, route_to: routeTo, user_decision_required: true }, physical_action: { gate_detected: true, decision_pending_in_parent_session: true }, confidence: "medium", }) ?? eventFile; notifyGate(cwd, planId, gate, routeTo, contextFile, result.agent); } return eventFile; } function simpleMessageText(message: any): string { const content = message?.content; if (typeof content === "string") return clipEnd(content, 1800); if (Array.isArray(content)) { return clipEnd(content.map((part: any) => part?.type === "text" ? part.text : "").filter(Boolean).join(" "), 1800); } return ""; } function simpleSessionDigest(ctx: any, maxItems = 10): string { const entries = ctx?.sessionManager?.getEntries?.() ?? []; const messages = entries.filter((e: any) => e?.type === "message" && e?.message?.role).slice(-maxItems).map((e: any) => e.message); if (!messages.length) return "- (no message digest available)"; return messages.map((m: any) => `- ${m.role === "assistant" ? "A" : m.role === "user" ? "U" : "M"}: ${simpleMessageText(m) || "(non-text message)"}`).join("\n"); } function gitValue(cwd: string, args: string[]): string { try { return spawnSync("git", args, { cwd, encoding: "utf8", timeout: 5000 }).stdout.trim(); } catch { return ""; } } function piCliFromShim(shimPath: string | undefined): string | undefined { if (!shimPath) return undefined; const candidate = path.join(path.dirname(shimPath), "node_modules", "@earendil-works", "pi-coding-agent", "dist", "cli.js"); try { return fs.existsSync(candidate) ? candidate : undefined; } catch { return undefined; } } function discoverWindowsPiCli(): string | undefined { const candidates: string[] = []; try { const found = spawnSync("where.exe", ["pi.cmd"], { encoding: "utf8", timeout: 5000 }).stdout ?? ""; for (const line of found.split(/\r?\n/).map((entry) => entry.trim()).filter(Boolean)) candidates.push(line); } catch {} for (const dir of (process.env.Path ?? process.env.PATH ?? "").split(path.delimiter).filter(Boolean)) { candidates.push(path.join(dir, "pi.cmd"), path.join(dir, "pi")); } for (const candidate of candidates) { const cli = piCliFromShim(candidate); if (cli) return cli; } return undefined; } export function piChildSpawnSpec(args: string[], platform = process.platform): { command: string; args: string[] } { if (platform === "win32") { const cli = discoverWindowsPiCli(); if (cli) return { command: process.execPath, args: [cli, ...args] }; return { command: "cmd.exe", args: ["/d", "/s", "/c", "pi.cmd", ...args] }; } return { command: "pi", args }; } function resolveProjectRoot(cwd: string): string { const gitRoot = gitValue(cwd, ["rev-parse", "--show-toplevel"]); return gitRoot || cwd || process.cwd(); } function savePidexMemory(ctx: any, argsLine?: string): string { const cwd = String(ctx?.cwd || process.cwd()); const projectRoot = resolveProjectRoot(cwd); const projectName = path.basename(projectRoot); const memoryDir = path.join(projectRoot, "wiki", "session-memory"); fs.mkdirSync(memoryDir, { recursive: true }); const now = new Date().toISOString(); const fileName = `${now.replace(/[:.]/g, "-")}.md`; const filePath = path.join(memoryDir, fileName); const hint = clipEnd(String(argsLine || ""), 700); const title = hint || "PIDEX session memory"; const body = `---\ntitle: ${title.replace(/\n/g, " ")}\ntype: session-memory\nstatus: active\ncreated: ${now.slice(0, 10)}\nupdated: ${now.slice(0, 10)}\nsource: pidex-session\n---\n\n# ${title}\n\n## Project\n\n- project_root: ${projectRoot}\n- project_name: ${projectName}\n- git_branch: ${gitValue(projectRoot, ["branch", "--show-current"]) || "unknown"}\n- git_commit: ${gitValue(projectRoot, ["rev-parse", "--short", "HEAD"]) || "unknown"}\n\n## Context\n\n- cwd: ${cwd}\n- session: ${ctx?.sessionManager?.getSessionFile?.() || "unknown"}\n- captured_at: ${now}\n\n## User note\n\n${hint || "(none)"}\n\n## Recent conversation digest\n\n${simpleSessionDigest(ctx)}\n\n## Navigation\n\n- Session memory index: [[index]]\n- Project index: [[../index]]\n`; fs.writeFileSync(filePath, body, "utf8"); const indexPath = path.join(memoryDir, "index.md"); let index = ""; try { index = fs.readFileSync(indexPath, "utf8"); } catch {} if (!index.trim()) { index = `---\ntitle: ${projectName} Session Memory\ntype: session-memory-index\nstatus: active\ncreated: ${now.slice(0, 10)}\nupdated: ${now.slice(0, 10)}\n---\n\n# ${projectName} Session Memory\n\n## Navigation\n\n- Project index: [[../index]]\n\n`; } index += `- ${now} — [[${fileName.replace(/\.md$/, "")}]] — ${title}\n`; fs.writeFileSync(indexPath, index, "utf8"); return filePath; } function runPidexQualityReport(cwd: string, argsLine?: string): { ok: boolean; summary: string } { const script = path.join(PACKAGE_ROOT, "scripts", "quality", "report.mjs"); const rawArgs = (argsLine ?? "").trim().split(/\s+/).filter(Boolean); const args = [script, "--project", cwd]; if (rawArgs.length) args.push(...rawArgs); else args.push("--since-last-review", "--last", "5"); const proc = spawnSync(process.execPath, args, { cwd: PACKAGE_ROOT, encoding: "utf8", timeout: 120_000 }); const stdout = (proc.stdout || "").trim(); const stderr = (proc.stderr || "").trim(); if (proc.status !== 0) { return { ok: false, summary: `pdq failed (${proc.status ?? "signal"})\n${stderr || stdout}` }; } try { const line = stdout.split(/\r?\n/).find((entry) => entry.startsWith("PIDEX_QUALITY_RESULT=")); const payload = JSON.parse(line ? line.slice("PIDEX_QUALITY_RESULT=".length) : stdout); return { ok: true, summary: [ "PIDEX quality report complete.", `Markdown: ${payload.markdown}`, `JSON: ${payload.json}`, payload.review_state ? `Review state: ${payload.review_state}` : undefined, payload.plans ? `Plans: ${(payload.plans ?? []).join(", ") || "none"}` : undefined, payload.project_mode ? `Project mode: ${payload.project_mode}` : undefined, Array.isArray(payload.mode_warnings) && payload.mode_warnings.length ? `Mode warnings: ${payload.mode_warnings.join("; ")}` : undefined, payload.confidence ? `Confidence: ${payload.confidence}` : undefined, payload.trace_gaps !== undefined ? `Trace gaps: ${payload.trace_gaps}` : undefined, ].filter(Boolean).join("\n"), }; } catch { return { ok: true, summary: stdout || "pdq completed with no output" }; } } export function recordAgentMetric(result: RpResult, cwd: string, task: string): string | undefined { try { const project = safePathSegment(cwd); const routing = extractRoutingBlock(result.finalText); const contextFile = extractRoutingField(routing, "context_file"); const planId = extractPlanId(task, result.finalText); const file = path.join(METRICS_DIR, project, `${planId}.jsonl`); fs.mkdirSync(path.dirname(file), { recursive: true }); const inputTokens = result.inputTokensEstimate ?? estimateTokensFromChars(result.inputChars ?? task.length); const outputTokens = result.outputTokensEstimate ?? estimateTokensFromChars(result.outputChars ?? result.finalText.length); const cost = result.costUsdEstimate ?? estimateCostUsd(result.model, inputTokens, outputTokens); const record = { timestamp: new Date().toISOString(), project: cwd, plan: planId, plan_uuid: extractPlanUuid(task, result.finalText), project_mode: projectModeForTelemetry(cwd), agent: result.agent, provider: result.provider, model: normalizeModelForPricing(result.model), duration_ms: result.durationMs, exit_code: result.exitCode, fallback_from: result.fallbackFrom, input_tokens_estimate: inputTokens, output_tokens_estimate: outputTokens, cache_read_tokens: result.cacheReadTokens, cache_write_tokens: result.cacheWriteTokens, cost_usd_estimate: cost, final_text_chars: result.finalText.length, agent_verdict: extractRoutingField(routing, "verdict"), route_to: extractRoutingField(routing, "route_to"), gate: extractRoutingField(routing, "gate"), routing_reason: extractRoutingField(routing, "reason"), context_file: contextFile, log_file: result.logFile, run_dir: result.runDir, session_dir: result.sessionDir, tool_count: result.toolCount, setup_error: result.setupError, route_source: result.routeSource, source: "pidex_agent", }; fs.appendFileSync(file, `${JSON.stringify(record)}\n`, { encoding: "utf8", mode: 0o600 }); result.inputTokensEstimate = inputTokens; result.outputTokensEstimate = outputTokens; result.costUsdEstimate = cost; return file; } catch (error: any) { result.warnings = [...(result.warnings ?? []), `Failed to record metrics: ${error?.message ?? error}`]; return undefined; } } type RpAuditOptions = { plan?: string; hours?: number; top: number; all: boolean; }; type RpAuditAgentStat = { calls: number; startTokens: number; toolChars: number; toolMsgs: number; readCalls: number; bashCalls: number; durationMs: number; }; function splitArgs(input?: string): string[] { if (!input) return []; return input.trim().split(/\s+/).filter(Boolean); } function parseRpAuditOptions(input?: string): { options?: RpAuditOptions; error?: string; help?: boolean } { const args = splitArgs(input); const options: RpAuditOptions = { top: 10, all: false, hours: 24 }; for (let i = 0; i < args.length; i += 1) { const a = args[i]; if (a === "help" || a === "--help" || a === "-h") return { help: true }; if (a === "--all") { options.all = true; continue; } if (a === "--plan" || a === "-p") { const v = args[i + 1]; if (!v) return { error: "Missing value for --plan" }; options.plan = v; i += 1; continue; } if (a === "--hours") { const v = Number.parseInt(args[i + 1] ?? "", 10); if (!Number.isFinite(v) || v <= 0) return { error: "--hours must be a positive integer" }; options.hours = v; i += 1; continue; } if (a === "--top" || a === "-t") { const v = Number.parseInt(args[i + 1] ?? "", 10); if (!Number.isFinite(v) || v <= 0) return { error: "--top must be a positive integer" }; options.top = v; i += 1; continue; } return { error: `Unknown argument: ${a}` }; } return { options }; } function rpAuditUsage(): string { return [ "Usage: /pidexaudit [--plan plan-61|61|unknown-plan] [--hours N] [--top N] [--all]", "Examples:", " /pidexaudit", " /pidexaudit --plan 61", " /pidexaudit --plan unknown-plan --all", " /pidexaudit --hours 6 --top 20", ].join("\n"); } function readTextMaybeGzip(filePath: string): string { const raw = fs.readFileSync(filePath); if (filePath.endsWith(".gz")) return gunzipSync(raw).toString("utf8"); return raw.toString("utf8"); } function normalizePlanArg(planRaw: string): string { const stripped = planRaw.trim().replace(/\.jsonl$/i, ""); if (!stripped) return ""; if (stripped === "unknown-plan") return stripped; if (/^plan-[0-9][a-zA-Z0-9._-]*$/i.test(stripped)) return stripped.toLowerCase(); if (/^[0-9][a-zA-Z0-9._-]*$/.test(stripped)) return `plan-${stripped.toLowerCase()}`; return stripped; } function formatInt(n: number): string { return new Intl.NumberFormat("en-US").format(Math.round(n)); } function estimateTokens(chars: number): number { return Math.ceil(chars / 4); } function runRpAudit(cwd: string, options: RpAuditOptions): { reportPath: string; summary: string; rows: number; } { const project = safePathSegment(cwd); const metricsDir = path.join(METRICS_DIR, project); if (!fs.existsSync(metricsDir)) { throw new Error(`No metrics directory for project: ${metricsDir}`); } const metricFiles = fs .readdirSync(metricsDir) .filter((name) => name.endsWith(".jsonl")) .map((name) => ({ name, path: path.join(metricsDir, name), mtime: fs.statSync(path.join(metricsDir, name)).mtimeMs, })) .sort((a, b) => b.mtime - a.mtime); if (metricFiles.length === 0) throw new Error(`No metric files found in ${metricsDir}`); let selectedFiles = metricFiles; if (options.plan) { const plan = normalizePlanArg(options.plan); const exact = metricFiles.find((f) => f.name === `${plan}.jsonl`); if (!exact) throw new Error(`Plan metrics not found: ${plan}.jsonl`); selectedFiles = [exact]; } else if (!options.all) { selectedFiles = [metricFiles[0]]; } const cutoff = !options.all && options.hours ? Date.now() - options.hours * 60 * 60 * 1000 : undefined; const rows: Array> = []; for (const file of selectedFiles) { const text = fs.readFileSync(file.path, "utf8"); for (const line of text.split("\n")) { if (!line.trim()) continue; try { const row = JSON.parse(line); if (cutoff && Date.parse(row.timestamp ?? "") < cutoff) continue; rows.push(row); } catch { // ignore bad metric lines } } } rows.sort((a, b) => String(a.timestamp ?? "").localeCompare(String(b.timestamp ?? ""))); if (rows.length === 0) throw new Error("No metric rows after filters."); const byAgent = new Map(); const fileReads = new Map(); let missingLogs = 0; let toolCharsTotal = 0; let toolMsgsTotal = 0; for (const row of rows) { const agent = String(row.agent ?? "unknown"); const stat = byAgent.get(agent) ?? { calls: 0, startTokens: 0, toolChars: 0, toolMsgs: 0, readCalls: 0, bashCalls: 0, durationMs: 0 }; stat.calls += 1; stat.startTokens += Number(row.input_tokens_estimate ?? 0); stat.durationMs += Number(row.duration_ms ?? 0); const logFile = row.log_file ? String(row.log_file) : ""; if (!logFile || !fs.existsSync(logFile)) { missingLogs += 1; byAgent.set(agent, stat); continue; } const logText = readTextMaybeGzip(logFile); for (const line of logText.split("\n")) { if (!line.trim()) continue; let event: any; try { event = JSON.parse(line); } catch { continue; } if (event?.type === "message_start" && event?.message?.role === "toolResult") { const parts = Array.isArray(event.message.content) ? event.message.content : []; const chars = parts .filter((p: any) => p?.type === "text") .reduce((sum: number, p: any) => sum + String(p.text ?? "").length, 0); stat.toolChars += chars; stat.toolMsgs += 1; toolCharsTotal += chars; toolMsgsTotal += 1; continue; } if (event?.type === "message_update" && event?.assistantMessageEvent?.type === "toolcall_end") { const tc = event.assistantMessageEvent.toolCall ?? {}; const name = String(tc.name ?? ""); if (name === "read") { const p = tc.arguments?.path ? String(tc.arguments.path) : ""; if (p) { stat.readCalls += 1; fileReads.set(p, (fileReads.get(p) ?? 0) + 1); } } else if (name === "bash") { stat.bashCalls += 1; } } } byAgent.set(agent, stat); } const agents = Array.from(byAgent.entries()).sort((a, b) => b[1].startTokens - a[1].startTokens); const topFiles = Array.from(fileReads.entries()) .sort((a, b) => b[1] - a[1]) .slice(0, options.top) .map(([filePath, count]) => { const bytes = fs.existsSync(filePath) ? fs.statSync(filePath).size : 0; return { filePath, count, bytes }; }); const lines: string[] = []; lines.push("# pidex Context Audit"); lines.push(""); lines.push(`- Generated: ${new Date().toISOString()}`); lines.push(`- Project: ${cwd}`); lines.push(`- Metric files: ${selectedFiles.map((f) => f.name).join(", ")}`); lines.push(`- Rows analyzed: ${rows.length}`); lines.push(`- Missing logs: ${missingLogs}`); lines.push(`- Tool result chars: ${formatInt(toolCharsTotal)} (~${formatInt(estimateTokens(toolCharsTotal))} tokens)`); lines.push(""); lines.push("## By agent"); lines.push(""); lines.push("| Agent | Calls | StartTokEst | ToolChars | ToolTokEst | ToolMsgs | ReadCalls | BashCalls | RuntimeMin | "); lines.push("|---|---:|---:|---:|---:|---:|---:|---:|---:|"); for (const [agent, stat] of agents) { lines.push(`| ${agent} | ${formatInt(stat.calls)} | ${formatInt(stat.startTokens)} | ${formatInt(stat.toolChars)} | ${formatInt(estimateTokens(stat.toolChars))} | ${formatInt(stat.toolMsgs)} | ${formatInt(stat.readCalls)} | ${formatInt(stat.bashCalls)} | ${(stat.durationMs / 60000).toFixed(1)} |`); } lines.push(""); lines.push(`## Top read files (top ${options.top})`); lines.push(""); lines.push("| Reads | EstTok(file) | Bytes | File |"); lines.push("|---:|---:|---:|---|"); for (const item of topFiles) { lines.push(`| ${formatInt(item.count)} | ${formatInt(estimateTokens(item.bytes))} | ${formatInt(item.bytes)} | ${item.filePath} |`); } lines.push(""); lines.push(`_Total tool messages: ${formatInt(toolMsgsTotal)}_`); const reportPath = path.join(os.tmpdir(), `pidexaudit-${project}-${Date.now()}.md`); fs.writeFileSync(reportPath, `${lines.join("\n")}\n`, { encoding: "utf8", mode: 0o600 }); const summary = `pidexaudit ok — rows=${rows.length}, agents=${agents.length}, toolTokens~${formatInt(estimateTokens(toolCharsTotal))}, report=${reportPath}`; return { reportPath, summary, rows: rows.length }; } export type DelegateAuthFailureKind = "launch" | "authentication"; export type DelegateAuthPreflightResult = { ok: boolean; output: string; failureKind?: DelegateAuthFailureKind }; export function delegateAuthInvocationSpec(pidexRoot: string, configPath: string, platform = process.platform): { command: string; args: string[]; cwd: string } { const pathApi = platform === "win32" ? path.win32 : path.posix; const cwd = pathApi.resolve(pidexRoot); const scriptPath = pathApi.join(cwd, "scripts", "delegate", "check-auth.sh"); const asBashArgument = (file: string): string => { const absolute = pathApi.isAbsolute(file) ? pathApi.resolve(file) : pathApi.resolve(cwd, file); const relative = pathApi.relative(cwd, absolute); const selected = relative && !pathApi.isAbsolute(relative) ? relative : absolute; return selected.replaceAll("\\", "/"); }; return { command: "bash", args: [asBashArgument(scriptPath), "--config", asBashArgument(configPath)], cwd }; } export function classifyDelegateAuthFailure(exitCode: number, output: string, timedOut = false, launchError = false): DelegateAuthFailureKind | undefined { if (!timedOut && !launchError && exitCode === 0) return undefined; if (timedOut || launchError || exitCode === 126 || exitCode === 127 || /no such file|not found|cannot execute|config file not found/i.test(output)) return "launch"; return "authentication"; } async function runDelegateAuthPreflight(timeoutMs = 15000): Promise { if (!fs.existsSync(CHECK_AUTH_SCRIPT)) return { ok: false, failureKind: "launch", output: "Delegate auth preflight launch/setup failed: checker script is missing." }; const invocation = delegateAuthInvocationSpec(PACKAGE_ROOT, CONFIG_PATH); let output = ""; let timedOut = false; let launchError = false; const exitCode = await new Promise((resolve) => { const proc = spawn(invocation.command, invocation.args, { cwd: invocation.cwd, stdio: ["ignore", "pipe", "pipe"], }); const timer = setTimeout(() => { timedOut = true; proc.kill("SIGTERM"); setTimeout(() => proc.kill("SIGKILL"), 5000).unref?.(); }, timeoutMs); proc.stdout.on("data", (data) => (output = appendTail(output, data.toString(), 16 * 1024))); proc.stderr.on("data", (data) => (output = appendTail(output, data.toString(), 16 * 1024))); proc.on("close", (code) => { clearTimeout(timer); resolve(code ?? 1); }); proc.on("error", (error) => { clearTimeout(timer); launchError = true; output = appendTail(output, `\nDelegate auth preflight launch/setup failed: ${error.message}`, 16 * 1024); resolve(1); }); }); if (timedOut) output = appendTail(output, "\nDelegate auth preflight launch/setup timed out; authentication was not checked.", 16 * 1024); const failureKind = classifyDelegateAuthFailure(exitCode, output, timedOut, launchError); return { ok: !failureKind, output: output.trim(), ...(failureKind ? { failureKind } : {}) }; } async function runRpAgent(params: { agent: string; task: string; cwd: string; model?: string; effort?: string; tools?: string[]; timeoutSeconds?: number; routeSource?: string; sandboxContext?: SandboxRuntimeContext; signal?: AbortSignal; onUpdate?: (text: string) => void; onProcessStarted?: (evidence?: { executionStartDigest: string }) => void; reviewExecution?: ReviewExecutionContext; closeoutExecution?: CloseoutExecutionContext; onCloseoutReturned?: (result: RpResult) => void; }): Promise { const observedExecution = params.reviewExecution ?? params.closeoutExecution; const startedAt = Date.now(); const agent = loadAgent(params.agent); const model = params.model ?? agent.frontmatter.model; params.onUpdate?.(`${formatAgentProgressLabel(params.agent)}: ${formatPiRunnerStartDetails(model, params.effort)}`); assertPiModelAllowed(model); const tools = params.tools === undefined ? parseTools(agent.frontmatter.tools) : params.tools; const maxTurns = parsePositiveInt(agent.frontmatter.maxTurns); const systemPrompt = buildAgentSystemPrompt(params.agent, agent.body, "pi"); if (observedExecution && (observedExecution.definitionDigest !== executionValueDigest(agent) || observedExecution.systemPromptDigest !== executionValueDigest(systemPrompt))) throw new Error("REVIEW_EXECUTION_SCOPE_CHANGED"); const inputChars = systemPrompt.length + params.task.length; const runLog = createAgentRunLog(params.agent, params.cwd); const systemPromptFile = path.join(runLog.runDir, `${safePathSegment(params.agent)}.system.md`); const taskFile = path.join(runLog.runDir, `${safePathSegment(params.agent)}.task.md`); await fs.promises.mkdir(runLog.sessionDir, { recursive: true }); await fs.promises.writeFile(systemPromptFile, systemPrompt, { encoding: "utf8", mode: 0o600 }); await fs.promises.writeFile(taskFile, `Task: ${params.task}`, { encoding: "utf8", mode: 0o600 }); try { const args = [ "--mode", "json", "-p", "--session-dir", runLog.sessionDir, "--no-skills", "--no-prompt-templates", "--no-themes", "--append-system-prompt", systemPromptFile, ]; if (shouldDisableChildExtensions(tools, params.sandboxContext)) args.push("--no-extensions"); if (model) args.push("--model", model); if (params.effort) args.push("--thinking", params.effort); if (tools !== undefined) args.push("--tools", tools.join(",")); args.push(`@${taskFile}`); let stderr = ""; let buffer = ""; let aborted = false; let timedOut = false; let turnLimitHit = false; let stdoutEventCount = 0; let skippedLargeEventCount = 0; let droppingOversizedLine = false; let toolCount = 0; let turnCount = 0; let inputTokens = 0; let outputTokens = 0; let cacheReadTokens = 0; let cacheWriteTokens = 0; let observedCost = 0; let finalText = ""; let observedModel = model; let proc: ReturnType | undefined; let observed: ReturnType | undefined; let executionStartDigest: string | undefined; let abortListener: (() => void) | undefined; const stopProcess = (signal: NodeJS.Signals, reason?: string) => { if (observed && reason) return observed.stop(reason); if (observed && signal === "SIGKILL") return observed.hardStop(); return proc?.kill(signal) ?? false; }; let cleanTerminalAssistantStopReceived = false; let forcedTerminationSignal = false; let finalDrainTimer: ReturnType | undefined; let finalHardKillTimer: ReturnType | undefined; const emitUpdate = createThrottledUpdate(params.onUpdate); const stderrStream = fs.createWriteStream(runLog.stderrFile, { flags: "a", mode: 0o600 }); const clearFinalDrainTimers = () => { if (finalDrainTimer) clearTimeout(finalDrainTimer); if (finalHardKillTimer) clearTimeout(finalHardKillTimer); finalDrainTimer = undefined; finalHardKillTimer = undefined; }; const startFinalDrain = () => { if (finalDrainTimer || !proc) return; finalDrainTimer = setTimeout(() => { if (!proc) return; forcedTerminationSignal = stopProcess("SIGTERM", "final_drain") || forcedTerminationSignal; finalHardKillTimer = setTimeout(() => { forcedTerminationSignal = stopProcess("SIGKILL") || forcedTerminationSignal; }, FINAL_STOP_HARD_KILL_MS); finalHardKillTimer.unref?.(); }, FINAL_STOP_GRACE_MS); finalDrainTimer.unref?.(); }; const parseLine = (line: string) => { if (!line.trim()) return; stdoutEventCount += 1; if (Buffer.byteLength(line, "utf8") > MAX_JSON_PARSE_LINE_BYTES) { skippedLargeEventCount += 1; return; } try { const event = JSON.parse(line); if (event?.type === "turn_start") { turnCount += 1; if (maxTurns && turnCount > maxTurns && proc) { turnLimitHit = true; stopProcess("SIGTERM", "turn_limit"); setTimeout(() => stopProcess("SIGKILL"), 5000).unref?.(); } } if (event?.type === "tool_execution_start") toolCount += 1; if (event?.type === "message_end" && event?.message?.role === "assistant") { const usage = event.message.usage; if (usage) { inputTokens += usage.input ?? usage.inputTokens ?? 0; outputTokens += usage.output ?? usage.outputTokens ?? 0; cacheReadTokens += usage.cacheRead ?? 0; cacheWriteTokens += usage.cacheWrite ?? 0; observedCost += usage.cost?.total ?? 0; } if (event.message.model) observedModel = event.message.model; const stopReason = event.message.stopReason; const hasToolCall = Array.isArray(event.message.content) && event.message.content.some((part: any) => part?.type === "toolCall"); if (stopReason === "stop" && !hasToolCall) { cleanTerminalAssistantStopReceived ||= !event.message.errorMessage; startFinalDrain(); } } const text = finalAssistantTextFromEvent(event); if (text) { finalText = text; emitUpdate(text); } } catch { // Ignore non-JSON noise. Raw line is already preserved in the gzip log. } }; const exitCode = await new Promise((resolve) => { const piSpec = piChildSpawnSpec(args); const childEnv = { ...process.env, [PIDEX_CHILD_ENV]: "1", [PIDEX_PROJECT_BOUNDARY_ENV]: JSON.stringify({ active: true, projectRoot: resolveProjectRoot(params.cwd), pidexRoot: PACKAGE_ROOT, startedCwd: params.cwd }), ...(params.sandboxContext ? { [PIDEX_SANDBOX_CONTEXT_ENV]: JSON.stringify(params.sandboxContext) } : {}), PI_SKIP_VERSION_CHECK: "1", }; if (observedExecution) { if (params.signal?.aborted) throw new Error("EXECUTION_DISPATCH_ABORTED"); observed = spawnObservedExecution({ stateRoot: observedExecution.stateRoot, binding: observedExecution.binding, command: piSpec.command, args: piSpec.args, cwd: params.cwd, env: childEnv, onProcessStarted: evidence => { executionStartDigest = evidence.executionStartDigest; params.onProcessStarted?.(evidence); } }); proc = observed.proc; } else { proc = spawn(piSpec.command, piSpec.args, { cwd: params.cwd, stdio: ["ignore", "pipe", "pipe"], env: childEnv }); params.onProcessStarted?.(); } const timeoutMs = params.timeoutSeconds ? Math.max(1, params.timeoutSeconds) * 1000 : undefined; const timeoutTimer = timeoutMs ? setTimeout(() => { timedOut = true; stopProcess("SIGTERM", "timeout"); setTimeout(() => stopProcess("SIGKILL"), 5000).unref?.(); }, timeoutMs) : undefined; proc.stdout.on("data", (data) => { if (!runLog.writeChunk(data)) { proc?.stdout.pause(); runLog.onDrain(() => proc?.stdout.resume()); } buffer += data.toString(); const lines = buffer.split("\n"); buffer = lines.pop() ?? ""; for (const line of lines) { if (droppingOversizedLine) { droppingOversizedLine = false; continue; } parseLine(line); } if (buffer.length > MAX_LINE_BUFFER_CHARS) { skippedLargeEventCount += 1; buffer = ""; droppingOversizedLine = true; } }); proc.stderr.on("data", (data) => { stderr = appendTail(stderr, data.toString()); stderrStream.write(data); }); proc.on("close", (code) => { if (timeoutTimer) clearTimeout(timeoutTimer); clearFinalDrainTimers(); stderrStream.end(); if (buffer.trim()) parseLine(buffer); const forcedDrainAfterFinalSuccess = forcedTerminationSignal && cleanTerminalAssistantStopReceived; resolve(forcedDrainAfterFinalSuccess ? 0 : forcedTerminationSignal ? (code ?? 1) : (code ?? 0)); }); proc.on("error", () => { if (timeoutTimer) clearTimeout(timeoutTimer); clearFinalDrainTimers(); stderrStream.end(); resolve(1); }); if (params.signal) { const kill = () => { aborted = true; if (params.reviewExecution && executionStartDigest) { const binding = params.reviewExecution.binding; recordReviewAbortHold({ stateDir: params.reviewExecution.stateRoot, project: binding.project, pipelineId: binding.pipelineId, identity: binding.identity, physical: binding.physical }); } stopProcess("SIGTERM", "user_abort"); setTimeout(() => stopProcess("SIGKILL"), 5000).unref?.(); }; abortListener = kill; if (params.signal.aborted) kill(); else params.signal.addEventListener("abort", kill, { once: true }); } }).finally(() => { if (abortListener) params.signal?.removeEventListener("abort", abortListener); stderrStream.end(); }); if (observedExecution) { const end = inspectExecution(observedExecution.stateRoot, observedExecution.binding, executionStartDigest); if (!["failed", "finished", "aborted"].includes(end.status)) throw new Error("REVIEW_EXECUTION_END_UNCONFIRMED"); aborted ||= end.status === "aborted"; timedOut ||= end.reason === "timeout"; turnLimitHit ||= end.reason === "turn_limit"; } emitUpdate(finalText, true); if (aborted) stderr = appendTail(stderr, "\nAborted by user."); if (timedOut) stderr = appendTail(stderr, `\nPi child timed out after ${params.timeoutSeconds}s.`); if (turnLimitHit) stderr = appendTail(stderr, `\nTurn limit exceeded (${maxTurns}).`); const warnings: string[] = []; if (skippedLargeEventCount > 0) warnings.push(`${skippedLargeEventCount} oversized child JSON events were logged but not parsed.`); if (finalText.length > MAX_AGENT_FINAL_CHARS) warnings.push(`Final response exceeded ${MAX_AGENT_FINAL_CHARS} chars; tool output may be truncated.`); const result: RpResult = { agent: params.agent, provider: "pi", model: observedModel, modelRequested: model, effort: params.effort, exitCode: (turnLimitHit || timedOut) && exitCode === 0 ? 1 : exitCode, stderr, finalText, logFile: runLog.logFile, runDir: runLog.runDir, sessionDir: runLog.sessionDir, stdoutEventCount, skippedLargeEventCount, toolCount, turnCount, maxTurns, durationMs: Date.now() - startedAt, inputChars, outputChars: finalText.length, inputTokensEstimate: inputTokens || undefined, outputTokensEstimate: outputTokens || undefined, cacheReadTokens: cacheReadTokens || undefined, cacheWriteTokens: cacheWriteTokens || undefined, costUsdEstimate: observedCost || undefined, routeSource: params.routeSource, timedOut, aborted, turnLimitHit, warnings: warnings.length ? warnings : undefined, }; // Durable closeout capture precedes metadata/metrics, hooks and parsers. params.onCloseoutReturned?.(result); try { fs.writeFileSync(path.join(runLog.runDir, "metadata.json"), JSON.stringify({ runner: "pi", provider: "pi", modelRequested: model, modelObserved: observedModel, effort: params.effort, route_source: params.routeSource, agent: params.agent, cwd: params.cwd, startedAt: new Date(startedAt).toISOString(), endedAt: new Date().toISOString(), durationMs: result.durationMs, exitCode: result.exitCode, toolCount, usage: { input: inputTokens, output: outputTokens, cacheRead: cacheReadTokens, cacheWrite: cacheWriteTokens, cost: observedCost, turns: turnCount, }, routing: { verdict: extractRoutingField(extractRoutingBlock(finalText), "verdict"), route_to: extractRoutingField(extractRoutingBlock(finalText), "route_to"), gate: extractRoutingField(extractRoutingBlock(finalText), "gate"), context_file: extractRoutingField(extractRoutingBlock(finalText), "context_file"), }, }, null, 2), { encoding: "utf8", mode: 0o600 }); } catch {} return result; } finally { try { await runLog.close(); } catch {} } } async function runCliDelegate(params: { agent: string; task: string; cwd: string; provider: string; model?: string; effort?: string; timeoutSeconds?: number; permissionMode?: AgentRoute["permission_mode"]; delegateTools?: string[]; allowedTools?: string[]; disallowedTools?: string[]; addDirs?: string[]; dangerouslySkipPermissions?: boolean; signal?: AbortSignal; onUpdate?: (text: string) => void; onProcessStarted?: () => void; }): Promise { const startedAt = Date.now(); const agent = loadAgent(params.agent); const provider = normalizeProvider(params.provider); const script = path.join(DELEGATE_DIR, `${provider}.sh`); if (provider !== "codex") { throw new Error(`Unsupported pidex delegate provider '${provider}' for ${params.agent}. PIDEX currently supports provider=pi and provider=codex only; use provider=pi for Pi-routed models.`); } if (!fs.existsSync(script)) throw new Error(`Delegate provider '${provider}' is configured but not installed: missing ${script}`); const tmp = await fs.promises.mkdtemp(path.join(os.tmpdir(), "pidex-delegate-")); const promptFile = path.join(tmp, `${params.agent}.prompt.md`); const outputFile = path.join(tmp, `${params.agent}.out.md`); const prompt = buildCliDelegatePrompt(params.agent, agent.body, params.task, provider); await fs.promises.writeFile(promptFile, prompt, { encoding: "utf8", mode: 0o600 }); params.onUpdate?.(`${formatAgentProgressLabel(params.agent)}: ${formatDelegateStartDetails(provider, params.model, params.effort)}`); let stderr = ""; let timedOut = false; let aborted = false; const timeoutMs = Math.max(1, params.timeoutSeconds ?? 300) * 1000; try { const exitCode = await new Promise((resolve) => { const proc = spawn("bash", [script], { cwd: params.cwd, stdio: ["ignore", "ignore", "pipe"], env: { ...process.env, PROMPT_FILE: promptFile, OUTPUT_FILE: outputFile, MODEL: params.model ?? "", EFFORT: params.effort ?? "", PERMISSION_MODE: params.permissionMode ?? "", DELEGATE_TOOLS: (params.delegateTools ?? []).join(","), ALLOWED_TOOLS: (params.allowedTools ?? []).join(","), DISALLOWED_TOOLS: (params.disallowedTools ?? []).join(","), ADD_DIRS: (params.addDirs ?? []).join(","), DANGEROUSLY_SKIP_PERMISSIONS: params.dangerouslySkipPermissions ? "1" : "", }, }); params.onProcessStarted?.(); const timer = setTimeout(() => { timedOut = true; proc.kill("SIGTERM"); setTimeout(() => proc.kill("SIGKILL"), 5000).unref?.(); }, timeoutMs); proc.stderr.on("data", (data) => (stderr = appendTail(stderr, data.toString()))); proc.on("close", (code) => { clearTimeout(timer); resolve(code ?? 0); }); proc.on("error", () => { clearTimeout(timer); resolve(1); }); if (params.signal) { const kill = () => { aborted = true; proc.kill("SIGTERM"); setTimeout(() => proc.kill("SIGKILL"), 5000).unref?.(); }; if (params.signal.aborted) kill(); else params.signal.addEventListener("abort", kill, { once: true }); } }); let finalText = ""; try { if (fs.existsSync(outputFile)) finalText = fs.readFileSync(outputFile, "utf8"); } catch {} if (timedOut) stderr = appendTail(stderr, `\nDelegate timed out after ${params.timeoutSeconds ?? 300}s.`); if (aborted) stderr = appendTail(stderr, "\nAborted by user."); const warnings: string[] = []; if (finalText.length > MAX_AGENT_FINAL_CHARS) warnings.push(`Final response exceeded ${MAX_AGENT_FINAL_CHARS} chars; tool output may be truncated.`); return { agent: params.agent, provider, model: params.model, modelRequested: params.model, effort: params.effort, exitCode, stderr, finalText, durationMs: Date.now() - startedAt, inputChars: prompt.length, outputChars: finalText.length, setupError: exitCode === 1, timedOut, aborted, warnings: warnings.length ? warnings : undefined, }; } finally { try { fs.rmSync(tmp, { recursive: true, force: true }); } catch {} } } export async function runConfiguredProviderAttempts( params: { provider: string; fallbackProvider?: string; reviewDispatch?: boolean; signal?: AbortSignal; retrySameProvider?: boolean; fallbackEnabled?: boolean; onSameProviderRetry?: () => void; onConfiguredFallback?: (missingRouting: boolean) => void; }, runProvider: (selectedProvider: string, fallbackFrom?: string) => Promise, ): Promise { if (params.reviewDispatch && params.signal?.aborted) throw new Error("REVIEW_DISPATCH_ABORTED"); let result = await runProvider(params.provider); let missingRouting = result.exitCode === 0 && result.finalText && !hasRoutingBlock(result.finalText); let shouldFallback = result.exitCode !== 0 || !result.finalText || missingRouting; if (shouldFallback && !params.reviewDispatch && !result.setupError && params.retrySameProvider) { params.onSameProviderRetry?.(); result = await runProvider(params.provider, params.provider); missingRouting = result.exitCode === 0 && result.finalText && !hasRoutingBlock(result.finalText); shouldFallback = result.exitCode !== 0 || !result.finalText || missingRouting; } if (shouldFallback && !params.reviewDispatch && !result.setupError && !params.retrySameProvider && params.fallbackEnabled && params.fallbackProvider && params.fallbackProvider !== params.provider) { params.onConfiguredFallback?.(Boolean(missingRouting)); result = await runProvider(params.fallbackProvider, params.provider); } return result; } type ReviewExecutionContext = { stateRoot: string; binding: any; definitionDigest: string; systemPromptDigest: string }; type CloseoutExecutionContext = Omit & { binding: { kind: "closeout"; project: string; pipelineId: string; planId: string; dispatchId: string; actor: string; scope: string } }; type CloseoutRequest = { action: "start" | "resume"; planId: string; pipelineId: string; artifactPath: string; dispatchId?: string }; const executionValueDigest = (value: unknown) => createHash("sha256").update(JSON.stringify(value)).digest("hex"); async function runConfiguredAgent(params: { agent: string; task: string; cwd: string; route?: HostAgentRoute; providerOverride?: string; modelOverride?: string; effortOverride?: string; tools?: string[]; sandboxContext?: SandboxRuntimeContext; signal?: AbortSignal; onUpdate?: (text: string) => void; onProcessStarted?: (evidence?: { executionStartDigest: string }) => void; reviewDispatch?: boolean; reviewExecution?: ReviewExecutionContext; closeoutExecution?: CloseoutExecutionContext; onCloseoutReturned?: (result: RpResult) => void; }): Promise { if (params.closeoutExecution && params.signal?.aborted) throw new Error("PIPELINE_CLOSEOUT_ABORT_HOLD"); params.onUpdate?.(`${formatAgentProgressLabel(params.agent)}: resolving route...`); const config = loadRoutingConfig(); const route: AgentRoute & { routeSource: string } = params.route ? { ...params.route, timeout_seconds: params.route.timeoutSeconds ?? params.route.timeout_seconds } : { ...resolveRoute(config, params.agent), routeSource: "configured-primary" }; const provider = normalizeProvider(params.providerOverride ?? route.provider); const timeoutSeconds = route.timeout_seconds; params.onUpdate?.(`${formatAgentProgressLabel(params.agent)}: ${formatConfiguredRouteStartDetails(route.routeSource, provider, route.model, route.effort)}`); const explicitTools = params.tools === undefined ? undefined : (normalizeToolList(params.tools) ?? []); const delegateTools = TOOL_FORWARDING_AGENTS.has(params.agent) ? (explicitTools ?? route.tools) : route.tools; const runProvider = async (selectedProvider: string, fallbackFrom?: string): Promise => { assertRuntimeBaseline(params.sandboxContext?.mode ?? "host-direct"); if (isToolHeavyAgent(params.agent) && !isPiProvider(selectedProvider) && !supportsDelegateToolLoop(selectedProvider)) { return { agent: params.agent, provider: selectedProvider, model: params.modelOverride, exitCode: 1, stderr: `Capability guard: ${params.agent} requires tool-capable execution. Provider '${selectedProvider}' delegate path does not support repository tool loop. Use provider=pi.`, finalText: "", fallbackFrom, setupError: true, warnings: [`Capability guard blocked ${params.agent} on provider '${selectedProvider}'.`], }; } // Agent route model/effort are provider-specific. If falling back to a // different provider, only keep explicit user overrides; otherwise let the // fallback provider use its own defaults/frontmatter. const selectedModel = params.modelOverride ?? (selectedProvider === provider ? route.model : undefined); const selectedEffort = params.effortOverride ?? (selectedProvider === provider ? route.effort : undefined); if (isPiProvider(selectedProvider)) { // When a delegated agent (e.g. Claude `sonnet`) falls back to Pi, do not // let delegate-only frontmatter model aliases leak into the Pi resolver. // Use the configured Pi default unless the caller explicitly overrode model. const piModel = selectedModel ?? (selectedProvider === provider ? undefined : config.defaults?.model); const result = await runRpAgent({ agent: params.agent, task: params.task, cwd: params.cwd, model: piModel, effort: selectedEffort, tools: explicitTools, timeoutSeconds, routeSource: route.routeSource, sandboxContext: params.sandboxContext, signal: params.signal, onUpdate: params.onUpdate, onProcessStarted: params.onProcessStarted, reviewExecution: params.reviewExecution, closeoutExecution: params.closeoutExecution, onCloseoutReturned: params.onCloseoutReturned, }); return { ...result, fallbackFrom }; } const result = await runCliDelegate({ agent: params.agent, task: params.task, cwd: params.cwd, provider: selectedProvider, model: selectedModel, effort: selectedEffort, timeoutSeconds, permissionMode: route.permission_mode, delegateTools: delegateTools, allowedTools: route.allowed_tools, disallowedTools: route.disallowed_tools, addDirs: route.add_dirs, dangerouslySkipPermissions: route.dangerously_skip_permissions, signal: params.signal, onUpdate: params.onUpdate, onProcessStarted: params.onProcessStarted, }); return { ...result, fallbackFrom }; }; const fallbackProviderRaw = config.fallback?.on_error?.trim().toLowerCase(); const fallbackDisabled = !fallbackProviderRaw || ["none", "off", "disabled", "false"].includes(fallbackProviderRaw); const configuredFallback = normalizeProvider(config.fallback?.on_error); const fallbackProvider = fallbackDisabled ? "" : (configuredFallback === "pi" || configuredFallback === "codex" ? configuredFallback : "pi"); const result = await runConfiguredProviderAttempts({ provider, fallbackProvider, reviewDispatch: params.reviewDispatch, signal: params.signal, retrySameProvider: isPiProvider(provider) && !params.providerOverride && !params.closeoutExecution, fallbackEnabled: !fallbackDisabled && !params.providerOverride && !params.route && !params.closeoutExecution, onSameProviderRetry: () => params.onUpdate?.(`${formatAgentProgressLabel(params.agent)}: ${provider} returned invalid completion; retrying once on ${provider}.`), onConfiguredFallback: (missingRouting) => params.onUpdate?.(`${formatAgentProgressLabel(params.agent)}: ${provider} failed${missingRouting ? " (missing ROUTING)" : ""}; falling back to ${fallbackProvider}.`), }, runProvider); if (result.setupError) { result.warnings = [ ...(result.warnings ?? []), "Delegate setup/auth error: not falling back to Pi because setup failures should be fixed before the pipeline continues.", ]; } result.routeSource = route.routeSource; result.metricsFile = recordAgentMetric(result, params.cwd, params.task); const operatorEventFile = recordOperatorEvents(result, params.cwd, params.task); if (operatorEventFile) { result.warnings = [...(result.warnings ?? []), `Operator events recorded: ${operatorEventFile}`]; } return result; } function shellEscapeForRegex(value: string): string { return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } function getGlobalGitHookStatus(): string { const expected = path.join(PACKAGE_ROOT, "modules", "pidex", "git-security-hooks", "scripts", "global"); const legacy = path.join(PACKAGE_ROOT, "scripts", "git-hooks", "global"); const current = spawnSync("git", ["config", "--global", "--get", "core.hooksPath"], { encoding: "utf8" }); const currentPath = current.status === 0 ? current.stdout.trim() : ""; const preCommit = path.join(expected, "pre-commit"); const commitMsg = path.join(expected, "commit-msg"); const executable = fs.existsSync(preCommit) && fs.existsSync(commitMsg); if (currentPath === expected && executable) return "installed/current"; if (currentPath === legacy) return `legacy PIDEX hook path active; run git-security-hooks.install capability to migrate (expected ${expected})`; if (!currentPath) return `not active; global core.hooksPath is unset (expected ${expected})`; return `not active; global core.hooksPath=${currentPath} (expected ${expected})`; } function runParallelAgentsCommand(args: string | undefined, cwd: string | undefined): { ok: boolean; summary: string } { const parts = (args ?? "").trim().split(/\s+/).filter(Boolean); const action = parts[0] || "status"; const runner = path.join(PACKAGE_ROOT, "scripts", "modules", "run-check.mjs"); const project = path.resolve(cwd ?? process.cwd(), "."); let passthrough: string[]; if (action === "status") passthrough = ["show"]; else if (action === "clear" && parts[1]) passthrough = ["clear", "--lane", parts[1]]; else return { ok: false, summary: "Usage: /pdparallel status | clear " }; const commandArgs = [runner, "--capability", "parallel-agents.status", "--agent", "orchestrator", "--phase", "planning", "--project", project, "--", ...passthrough]; const proc = spawnSync(process.execPath, commandArgs, { cwd: PACKAGE_ROOT, encoding: "utf8", timeout: 120_000 }); const output = `${proc.stdout ?? ""}\n${proc.stderr ?? ""}`.trim(); return { ok: proc.status === 0, summary: clipEnd(output || `pdparallel ${action} exit=${proc.status}`, 1600) }; } function runWikiHygieneAudit(projectRoot: string): { ok: boolean; summary: string; reportMd?: string } { const runner = path.join(PACKAGE_ROOT, "scripts", "modules", "run-check.mjs"); const proc = spawnSync(process.execPath, [runner, "--capability", "memory-wiki-hygiene.check", "--agent", "pidex-wiki-hygienist", "--phase", "maintenance", "--project", projectRoot], { cwd: PACKAGE_ROOT, encoding: "utf8", timeout: 120_000 }); const output = `${proc.stdout ?? ""}\n${proc.stderr ?? ""}`.trim(); const line = (proc.stdout ?? "").split(/\r?\n/).find((entry) => entry.startsWith("PIDEX_WIKI_HYGIENE_RESULT=")); if (proc.status !== 0 || !line) { return { ok: false, summary: `Wiki hygiene audit failed${proc.status !== null ? ` exit=${proc.status}` : ""}: ${clipEnd(output, 1200)}` }; } try { const parsed = JSON.parse(line.slice("PIDEX_WIKI_HYGIENE_RESULT=".length)); const reportMd = parsed.report_md || parsed.markdown; const summary = `Wiki hygiene audit complete: ${reportMd} (score=${parsed.score}, critical=${parsed.critical ?? parsed.counts?.critical ?? 0}, high=${parsed.high ?? parsed.counts?.high ?? 0})`; return { ok: true, summary, reportMd }; } catch (error: any) { return { ok: false, summary: `Wiki hygiene audit result parse failed: ${error?.message ?? error}` }; } } function canonicalHomeStatus(): { ok: boolean; path: string; message?: string } { const homeRoot = path.resolve(CANONICAL_HOME_ROOT); if (isPidexRuntimeRoot(homeRoot)) return { ok: true, path: homeRoot }; if (fs.existsSync(homeRoot)) { return { ok: false, path: homeRoot, message: `PIDEX canonical runtime root exists but is not a valid PIDEX checkout: ${homeRoot}`, }; } return { ok: false, path: homeRoot, message: `PIDEX canonical runtime root is missing: ${homeRoot}`, }; } function canonicalHomeMissingMessage(): string { return [ "PIDEX is installed as a Pi bootstrap package, but the canonical runtime checkout is not initialized.", `Required path: ${path.resolve(CANONICAL_HOME_ROOT)}`, "", "Run:", " /pidex-init-home", "", "This will clone PIDEX into the canonical runtime checkout, run the platform installer, and remove the bootstrap package registration so /reload uses the canonical checkout.", ].join("\n"); } function runCommandForInit(command: string, args: string[], cwd?: string): { ok: boolean; output: string } { const proc = spawnSync(command, args, { cwd, encoding: "utf8", timeout: 20 * 60_000 }); const output = [proc.stdout, proc.stderr].filter(Boolean).join("\n").trim(); return { ok: proc.status === 0, output }; } async function initializePidexHome(ctx: any): Promise { const status = canonicalHomeStatus(); if (status.ok) { await ctx.ui.notify(`PIDEX canonical runtime root already exists: ${status.path}`, "info"); return; } if (fs.existsSync(status.path)) { await ctx.ui.notify(`${status.message}\nMove or remove that directory before running /pidex-init-home.`, "error"); return; } if (ctx.hasUI) { const installer = process.platform === "win32" ? "install.windows.ps1" : "install.sh"; const choice = await ctx.ui.select(`Initialize PIDEX canonical runtime root at ${status.path}?\n\nThis clones https://github.com/d-trattner/pidex.git, runs ${installer}, then removes the npm/git bootstrap package registration if present.`, ["Initialize", "Cancel"]); if (choice !== "Initialize") return; } const parent = path.dirname(status.path); fs.mkdirSync(parent, { recursive: true }); let result = runCommandForInit("git", ["clone", "--", "https://github.com/d-trattner/pidex.git", status.path], parent); if (!result.ok) { await ctx.ui.notify(`PIDEX clone failed:\n${result.output}`, "error"); return; } if (process.platform === "win32") { result = runCommandForInit("powershell.exe", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", "install.windows.ps1", "-NonInteractive"], status.path); } else { result = runCommandForInit("bash", ["install.sh", "--skip-global-git-hook"], status.path); } if (!result.ok) { const installer = process.platform === "win32" ? "install.windows.ps1" : "install.sh"; await ctx.ui.notify(`PIDEX ${installer} failed. Checkout remains at ${status.path}.\n\n${result.output}`, "error"); return; } const cleanupSpecs = [ "npm:@d-trattner/pidex", BOOTSTRAP_PACKAGE_VERSION ? `npm:@d-trattner/pidex@${BOOTSTRAP_PACKAGE_VERSION}` : undefined, "git:https://github.com/d-trattner/pidex", ].filter(Boolean) as string[]; const cleanup = cleanupSpecs.map((spec) => runCommandForInit("pi", ["remove", spec])); const cleanupNotes = cleanup.map((entry) => entry.output).filter(Boolean).join("\n"); await ctx.ui.notify([ `PIDEX initialized at ${status.path}.`, "Run /reload so Pi loads the canonical PIDEX checkout.", cleanupNotes ? `\nBootstrap cleanup output:\n${cleanupNotes}` : "", ].join("\n"), "info"); } function sensitiveSandboxPath(value: string | undefined): string | undefined { if (!value) return undefined; const normalized = value.replaceAll("\\", "/"); const patterns = [ /(^|\/)\.env($|\.)/, /(^|\/)\.npmrc$/, /(^|\/)\.yarnrc(\.yml)?$/, /(^|\/)\.git-credentials$/, /(^|\/)\.netrc$/, /(^|\/)auth\.json$/i, /(^|\/)secrets?\//, /\.(pem|key|crt|p12|pfx|kubeconfig)$/, /(^|\/)\.ssh\//, /(^|\/)\.aws\//, /(^|\/)\.codex\//, /(^|\/)AppData\/(Roaming|Local)\/.*(auth|token|credential)/i, ]; return patterns.some((pattern) => pattern.test(normalized)) ? normalized : undefined; } const SAFE_SANDBOX_HOST_HELPERS = new Set([ "probe.mjs", "exec.mjs", "run-command.mjs", "status.mjs", "diff.mjs", "apply.mjs", "extract-artifacts.mjs", "cleanup.mjs", ]); function sandboxRuntimeHelperMarker(): string { return ["modules", "pidex", "sandbox-runtime", "scripts", "sandbox", ""].join("/"); } function canonicalSandboxHelperPath(helperName: string): string | undefined { if (!SAFE_SANDBOX_HOST_HELPERS.has(helperName)) return undefined; const candidate = path.join(PACKAGE_ROOT, "modules", "pidex", "sandbox-runtime", "scripts", "sandbox", helperName); try { return fs.realpathSync(candidate); } catch { return undefined; } } export function commandMentionsSandboxRuntimeHelper(command: string): boolean { return command.replaceAll("\\", "/").includes(sandboxRuntimeHelperMarker()); } function splitStrictHostHelperCommand(command: string): string[] | undefined { const raw = String(command || ""); if (!raw.trim()) return []; if (/[\r\n`<>|;&]/.test(raw) || /\$\s*\(/.test(raw) || /\b(?:&&|\|\|)\b/.test(raw)) return undefined; if (/["']/.test(raw)) return undefined; const tokens = raw.trim().split(/\s+/); if (tokens.some((token) => !/^[A-Za-z0-9_@%+=:,./\\-]+$/.test(token))) return undefined; return tokens; } export function sandboxHostBashAllowed(command: string): boolean { const tokens = splitStrictHostHelperCommand(command); if (!tokens) return false; if (tokens.length === 0) return true; const nodeBin = path.basename(tokens[0]).toLowerCase(); if (nodeBin !== "node" && nodeBin !== "node.exe") return false; const helper = tokens[1]; if (!helper || !path.isAbsolute(helper)) return false; const helperName = path.basename(helper); const canonical = canonicalSandboxHelperPath(helperName); if (!canonical) return false; try { const resolved = path.resolve(helper); return resolved === canonical && !fs.lstatSync(resolved).isSymbolicLink() && fs.realpathSync(resolved) === canonical; } catch { return false; } } function readSandboxRuntimeContext(): SandboxRuntimeContext | undefined { const raw = process.env[PIDEX_SANDBOX_CONTEXT_ENV]; if (!raw) return undefined; try { const parsed = JSON.parse(raw); if (parsed?.mode !== "hardened-pipeline") return undefined; const hostProjectRoot = path.resolve(String(parsed.hostProjectRoot || "")); const sandboxWorkspace = path.resolve(String(parsed.sandboxWorkspace || "")); const allowedWriteRoot = path.resolve(String(parsed.allowedWriteRoot || sandboxWorkspace)); if (!hostProjectRoot || !sandboxWorkspace || !allowedWriteRoot) return undefined; return { mode: "hardened-pipeline", runId: String(parsed.runId || ""), hostProjectRoot, sandboxWorkspace, allowedWriteRoot }; } catch { return undefined; } } function pathWithin(root: string, target: string): boolean { const rel = path.relative(path.resolve(root), path.resolve(target)); return rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel)); } export type ProjectBoundaryPathEvidenceAdapter = { platform: string; pathApi: any; lstatSync: (value: string) => { isDirectory: () => boolean; isSymbolicLink?: () => boolean }; realpathNative: (value: string) => string; }; type ProjectBoundaryPathIdentity = | { tag: "success"; canonicalPath: string; lexicalPath: string; evidenceKind: "existing" | "ancestor-reconstructed" } | { tag: "failure"; category: string; stage: string }; type ProjectBoundaryPathPurpose = "existing-root" | "existing-target" | "prospective-write"; function defaultProjectBoundaryPathEvidence(): ProjectBoundaryPathEvidenceAdapter { return { platform: process.platform, pathApi: path, lstatSync: fs.lstatSync, realpathNative: fs.realpathSync.native }; } function identityFailure(category: string, stage: string): ProjectBoundaryPathIdentity { return { tag: "failure", category, stage }; } function classifyWin32AbsolutePath(value: string): string | undefined { const native = String(value || "").replaceAll("/", "\\"); let ordinary: string; if (/^\\\\\?\\[A-Za-z]:\\/.test(native)) ordinary = native.slice(4); else if (/^\\\\\?\\UNC\\[^\\]+\\[^\\]+(?:\\|$)/i.test(native)) ordinary = `\\\\${native.slice(8)}`; else ordinary = native; if (/^[A-Za-z]:\\/.test(ordinary)) return ordinary; if (/^\\\\(?![?.]\\)[^\\]+\\[^\\]+(?:\\|$)/.test(ordinary)) return ordinary; return undefined; } function normalizeEvidencePath(value: string, adapter: ProjectBoundaryPathEvidenceAdapter): string | undefined { const canonical = String(value || ""); const normalized = adapter.platform === "win32" ? classifyWin32AbsolutePath(canonical) : canonical; return normalized && adapter.pathApi.isAbsolute(normalized) ? adapter.pathApi.resolve(normalized) : undefined; } function lexicalBoundaryPath(value: string, cwd: string, adapter: ProjectBoundaryPathEvidenceAdapter): string | ProjectBoundaryPathIdentity { let raw = String(value || ""); if (!raw.trim() || raw.includes("\0")) return identityFailure("invalid-input", "lexicalization"); if (adapter.platform === "win32") { if (/^\/[a-zA-Z]\//.test(raw)) raw = `${raw[1]}:/${raw.slice(3)}`; if (/^\/mnt\/[a-zA-Z]\//.test(raw)) raw = `${raw[5]}:/${raw.slice(7)}`; const native = raw.replaceAll("/", "\\"); if (/^[A-Za-z]:\\/.test(native) || native.startsWith("\\\\") || native.startsWith("\\??\\")) { const classified = classifyWin32AbsolutePath(native); if (!classified) return identityFailure("unsupported-namespace", "namespace-classification"); raw = classified; } } const resolved = adapter.pathApi.resolve(cwd, raw); return adapter.pathApi.isAbsolute(resolved) ? resolved : identityFailure("invalid-input", "lexicalization"); } function validReconstructionSegment(segment: string, adapter: ProjectBoundaryPathEvidenceAdapter): boolean { if (!segment || segment === "." || segment === ".." || segment.includes("\0") || /[\\/]/.test(segment) || adapter.pathApi.isAbsolute(segment)) return false; if (adapter.platform !== "win32") return true; if (segment.includes(":") || /[. ]$/.test(segment)) return false; const device = segment.replace(/[. ]+$/, "").split(".")[0].toUpperCase(); return !/^(?:CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])$/.test(device); } export function resolveProjectBoundaryPathIdentity(value: string, cwd: string, purpose: ProjectBoundaryPathPurpose, injected?: ProjectBoundaryPathEvidenceAdapter): ProjectBoundaryPathIdentity { const adapter = injected ?? defaultProjectBoundaryPathEvidence(); const lexical = lexicalBoundaryPath(value, cwd, adapter); if (typeof lexical !== "string") return lexical; let candidate = lexical; const suffix: string[] = []; for (;;) { let stat: { isDirectory: () => boolean; isSymbolicLink?: () => boolean }; try { stat = adapter.lstatSync(candidate); } catch (error: any) { if (error?.code !== "ENOENT") return identityFailure("permission-io-denial", "evidence-probe"); if (purpose !== "prospective-write") return identityFailure(purpose === "existing-root" ? "absent-required-root" : "absent-required-target", "evidence-probe"); const parent = adapter.pathApi.dirname(candidate); if (parent === candidate) return identityFailure("root-exhaustion", "evidence-probe"); suffix.unshift(adapter.pathApi.basename(candidate)); candidate = parent; continue; } if (suffix.length && !stat.isDirectory()) return identityFailure("non-directory-ancestor", "evidence-probe"); let canonicalAncestor: string | undefined; try { canonicalAncestor = normalizeEvidencePath(adapter.realpathNative(candidate), adapter); } catch { return identityFailure(stat.isSymbolicLink?.() ? "broken-link" : "existing-but-unresolvable", "native-realpath"); } if (!canonicalAncestor) return identityFailure("canonical-mismatch", "native-realpath"); if (!suffix.length) return { tag: "success", canonicalPath: canonicalAncestor, lexicalPath: lexical, evidenceKind: "existing" }; if (!suffix.every((segment) => validReconstructionSegment(segment, adapter))) return identityFailure("invalid-prospective-segment", "remainder-validation"); return { tag: "success", canonicalPath: adapter.pathApi.join(canonicalAncestor, ...suffix), lexicalPath: lexical, evidenceKind: "ancestor-reconstructed" }; } } function boundaryPathWithin(root: string, target: string, adapter: ProjectBoundaryPathEvidenceAdapter): boolean { const rootResolved = adapter.pathApi.resolve(root); const targetResolved = adapter.pathApi.resolve(target); if (adapter.platform === "win32") { const rel = adapter.pathApi.relative(rootResolved.toLowerCase(), targetResolved.toLowerCase()); return rel === "" || (!rel.startsWith("..") && !adapter.pathApi.isAbsolute(rel)); } return pathWithin(rootResolved, targetResolved); } function projectBoundaryBlock(toolName: string, pathForm: string, category: string, stage: string): { block: true; reason: string } { return { block: true, reason: `PIDEX project boundary blocks ${toolName} ${pathForm}: ${category}/${stage}.` }; } function allowedPidexRuntimeWrite(pidexRoot: string, target: string, adapter: ProjectBoundaryPathEvidenceAdapter): boolean { const rel = adapter.pathApi.relative(adapter.pathApi.resolve(pidexRoot), adapter.pathApi.resolve(target)).replaceAll("\\", "/"); return /^(state\/(runs|metrics|modules|pipeline-events|orchestrator-events|provider-limits|sandbox)(\/|$))/.test(rel); } type ProjectBoundaryContext = { active: true; projectRoot: string; pidexRoot: string; startedCwd: string }; function readProjectBoundaryContext(ctx: any, adapter: ProjectBoundaryPathEvidenceAdapter): ProjectBoundaryContext | undefined { const raw = process.env[PIDEX_PROJECT_BOUNDARY_ENV]; if (raw) { try { const parsed = JSON.parse(raw); if (parsed?.active && parsed.projectRoot) return { active: true, projectRoot: String(parsed.projectRoot), pidexRoot: String(parsed.pidexRoot || PACKAGE_ROOT), startedCwd: String(parsed.startedCwd || parsed.projectRoot) }; } catch {} } const cwd = adapter.pathApi.resolve(String(ctx?.cwd || process.cwd())); return { active: true, projectRoot: resolveProjectRoot(cwd), pidexRoot: PACKAGE_ROOT, startedCwd: cwd }; } function toolPathCandidate(input: any): string | undefined { if (!input || typeof input !== "object") return undefined; const keys = ["path", "file", "filepath", "filePath", "target", "targetPath"]; for (const key of keys) if (typeof input[key] === "string" && input[key].trim()) return input[key]; return undefined; } export function inspectProjectBoundaryToolCall(event: any, ctx: any, injected?: ProjectBoundaryPathEvidenceAdapter): { block: boolean; reason: string } | undefined { const adapter = injected ?? defaultProjectBoundaryPathEvidence(); const boundary = readProjectBoundaryContext(ctx, adapter); if (!boundary) return undefined; const toolName = String(event?.toolName || ""); const cwd = adapter.pathApi.resolve(String(ctx?.cwd || boundary.startedCwd || boundary.projectRoot)); if (toolName === "read" || toolName === "write" || toolName === "edit") { const raw = toolPathCandidate(event?.input); if (!raw) return toolName === "read" ? undefined : { block: true, reason: `PIDEX project boundary blocks ${toolName} without an explicit file path.` }; const rawSensitive = sensitiveSandboxPath(raw); if (rawSensitive) return projectBoundaryBlock(toolName, "raw-path", "sensitive-path", "raw-policy"); const projectRoot = resolveProjectBoundaryPathIdentity(boundary.projectRoot, cwd, "existing-root", adapter); if (projectRoot.tag === "failure") return projectBoundaryBlock(toolName, "project-root", projectRoot.category, projectRoot.stage); const pidexRoot = resolveProjectBoundaryPathIdentity(boundary.pidexRoot, cwd, "existing-root", adapter); if (pidexRoot.tag === "failure") return projectBoundaryBlock(toolName, "pidex-root", pidexRoot.category, pidexRoot.stage); const target = resolveProjectBoundaryPathIdentity(raw, cwd, toolName === "write" ? "prospective-write" : "existing-target", adapter); if (target.tag === "failure") return projectBoundaryBlock(toolName, "target-path", target.category, target.stage); if (sensitiveSandboxPath(target.lexicalPath)) return projectBoundaryBlock(toolName, "lexical-path", "sensitive-path", "lexical-policy"); if (sensitiveSandboxPath(target.canonicalPath)) return projectBoundaryBlock(toolName, "canonical-path", "sensitive-path", "canonical-policy"); if (toolName === "read") { if (boundaryPathWithin(projectRoot.canonicalPath, target.canonicalPath, adapter) || boundaryPathWithin(pidexRoot.canonicalPath, target.canonicalPath, adapter)) return undefined; return projectBoundaryBlock(toolName, "outside-project-or-pidex", "outside-project-or-pidex", "comparison"); } if (boundaryPathWithin(projectRoot.canonicalPath, target.canonicalPath, adapter)) return undefined; if (boundaryPathWithin(pidexRoot.canonicalPath, target.canonicalPath, adapter) && allowedPidexRuntimeWrite(pidexRoot.canonicalPath, target.canonicalPath, adapter)) return undefined; return projectBoundaryBlock(toolName, "outside-project-or-pidex", "outside-project-or-pidex", "comparison"); } if (toolName === "bash") { const command = String(event?.input?.command || ""); if (!command) return undefined; const compact = command.replace(/\\\n/g, " ").replace(/\s+/g, " ").trim(); if (/\bgit\b[^;&|]*\badd\b[^;&|]*(?:^|\s)(?:-f|--force)(?:\s|$)/i.test(compact)) { return { block: true, reason: "PIDEX blocks git add -f/--force in parent and child sessions. Ignored runtime state must remain uncommitted." }; } const highRisk = [ /\bgit\s+config\s+--global\b/i, /\b(?:npm|pnpm|yarn)\s+config\s+set\b/i, /\bpi\s+(?:install|remove)\b/i, /\bdocker\s+run\b[\s\S]*-v\s+(?:\/|[A-Za-z]:\\|[A-Za-z]:\/):\/host/i, /\brm\s+-rf\s+(?:\/|[A-Za-z]:[\\/])/i, /\bdel\s+\/s\s+[A-Za-z]:[\\/]/i, /\bRemove-Item\b[\s\S]*[A-Za-z]:[\\/]/i, ]; if (highRisk.some((pattern) => pattern.test(compact))) return { block: true, reason: "PIDEX project boundary blocks high-risk host/global mutation command." }; } return undefined; } export function inspectSandboxToolCall(event: any, ctx: any): { block: boolean; reason: string } | undefined { const active = readSandboxRuntimeContext(); if (!active) return undefined; const toolName = String(event?.toolName || ""); if (toolName === "read") { const blocked = sensitiveSandboxPath(toolPathCandidate(event?.input)); if (blocked) return { block: true, reason: `PIDEX sandbox blocks reads of env/secret/runtime paths while active: ${blocked}` }; return undefined; } if (toolName === "write" || toolName === "edit") { const raw = toolPathCandidate(event?.input); if (!raw) return { block: true, reason: `PIDEX sandbox blocks ${toolName} without an explicit file path.` }; const resolved = path.isAbsolute(raw) ? path.resolve(raw) : path.resolve(ctx?.cwd || active.sandboxWorkspace, raw); if (pathWithin(active.allowedWriteRoot, resolved)) return undefined; return { block: true, reason: `PIDEX sandbox blocks ${toolName} outside sandbox workspace. target=${resolved}; allowed=${active.allowedWriteRoot}` }; } if (toolName === "bash") { const command = event?.input?.command; if (typeof command !== "string") return { block: true, reason: "PIDEX sandbox blocks bash without an explicit command while sandbox context is active." }; if (sandboxHostBashAllowed(command)) return undefined; const helperDetail = commandMentionsSandboxRuntimeHelper(command) ? " Malformed sandbox helper attempts must use one exact canonical node invocation without shell chaining, pipes, redirects, substitutions, or quotes." : ""; return { block: true, reason: `PIDEX sandbox blocks raw host bash while sandbox context is active. Use canonical sandbox runtime helpers or operate through the sandbox/Docker runner.${helperDetail}` }; } return undefined; } export function inspectBashForGitHookRisk(command: string): { block?: string; warn?: string } | undefined { const compact = command.replace(/\\\n/g, " ").replace(/\s+/g, " ").trim(); if (!compact) return undefined; if (/\bgit\b[^;&|]*\badd\b[^;&|]*(?:^|\s)(?:-f|--force)(?:\s|$)/i.test(compact)) { return { block: "PIDEX blocks git add -f/--force. Ignored runtime state must remain uncommitted; change ignore policy for genuine source files instead." }; } if (/\bgit\b[\s\S]*\bcommit\b[\s\S]*(--no-verify|--no-gpg-sign)/.test(compact)) { return { block: "PIDEX blocks git commit bypass flags (--no-verify/--no-gpg-sign). Remove the flag and let the security hook run." }; } const escapedRoot = shellEscapeForRegex(PACKAGE_ROOT); const tamperPatterns = [ /git\s+config\s+(--global\s+)?(--unset\s+)?core\.hooksPath/, /rm\s+.*\.git\/hooks\/(pre-commit|commit-msg)/, /chmod\s+-x\s+.*\.git\/hooks\/(pre-commit|commit-msg)/, new RegExp(`rm\\s+.*${escapedRoot}/.*git-security-hooks/scripts/global/(pre-commit|commit-msg)`), new RegExp(`chmod\\s+-x\\s+.*${escapedRoot}/.*git-security-hooks/scripts/global/(pre-commit|commit-msg)`), ]; if (tamperPatterns.some((pattern) => pattern.test(compact))) { return { warn: "This command may modify Git hook protection. Continue only if intentionally installing/uninstalling or repairing PIDEX hooks." }; } return undefined; } const PidexProjectParams = Type.Object({ command: Type.String({ description: "Read-only Project Pipeline command: status, diagnose, runs, show-run, artifacts, credentials-status, preview-status, or preview-logs." }), projectId: Type.Optional(Type.String({ description: "Project Pipeline project id, usually pp-... Required except for status." })), runId: Type.Optional(Type.String({ description: "Project Pipeline run id. Required for show-run." })), }); export const PidexProjectMaintenanceParams = Type.Object({ action: Type.Literal("upgrade-pi", { description: "Upgrade only the existing sandbox's Pi CLI to the exact Dockerfile pin. Requires interactive user approval." }), projectId: Type.String({ pattern: "^[a-z0-9][a-z0-9_.-]{2,80}$", description: "Exact registered Project Pipeline project id. Ask the user if the target is ambiguous." }), }, { additionalProperties: false }); export async function requestProjectPiMaintenance(params: any, ctx: any, options: { signal?: AbortSignal; run?: typeof runPdProjectCommand } = {}): Promise<{ ok: boolean; summary: string; no_fallback?: true }> { if (!params || typeof params !== "object" || Array.isArray(params) || Object.keys(params).length !== 2 || !Object.hasOwn(params, "action") || !Object.hasOwn(params, "projectId") || params.action !== "upgrade-pi" || typeof params.projectId !== "string" || !/^[a-z0-9][a-z0-9_.-]{2,80}$/.test(params.projectId) || params.projectId.includes("..")) throw new Error("invalid maintenance request"); const projectId = params.projectId; // Capture the approved target before awaiting UI. const denied = (summary: string) => ({ ok: false, no_fallback: true as const, summary }); if (process.env[PIDEX_CHILD_ENV] === "1" || process.env.PIDEX_PROJECT_PIPELINE_CHILD === "1" || process.env.PIDEX_PROJECT_PIPELINE_CONTAINER === "1") return denied("Pi maintenance is available only in the host main session; no changes made."); if (!ctx?.hasUI || typeof ctx.ui?.select !== "function") return denied("Pi maintenance requires interactive user approval. No changes made; use an interactive host Pi session."); if (options.signal?.aborted) return denied("Pi maintenance cancelled before execution; no changes made."); let choice: string | undefined; try { choice = await ctx.ui.select(`Upgrade Dockerfile-pinned Pi as container root for ${projectId}? Downloads npm packages; no image replacement, project-data changes or pipeline continuation.`, ["Cancel", "Upgrade Pi"]); } catch { return denied("Pi maintenance approval unavailable; no changes made."); } if (choice !== "Upgrade Pi" || options.signal?.aborted) return denied("Pi maintenance not approved or cancelled; no changes made."); return (options.run ?? runPdProjectCommand)({ command: "upgrade-pi", projectId, confirm: projectId }, { projectRoot: ctx.cwd ?? process.cwd() }); } type HostAgentBoundaryOptions = { agentCwd: string; agentProjectMode?: ProjectPipelineModeResult; signal?: AbortSignal; onUpdate?: (text: string) => void; loadEligibleLanes?: (params: HostAgentRequest) => EligibleParallelLane[]; loadConfig?: () => RoutingConfig; resolveSandboxState?: () => Pick; probeSandbox?: () => { ok: boolean; summary: string }; runConfigured?: typeof runConfiguredAgent; runSandboxed?: typeof runSandboxedConfiguredAgent; reviewLifecycle?: { stateDir: string; pipelineId: string }; automaticRetrospectiveSource?: () => AutomaticRetrospectiveLearningSource | undefined; }; const REVIEWER_AGENTS = new Set(["pidex-critic", "pidex-code-reviewer", "pidex-security", "pidex-qa"]); const CORRECTION_OWNERS = new Set(["pidex-planner", "pidex-implementer"]); const REVIEW_GATES = ["critic", "code-review", "security", "qa"]; const REVIEW_MODES = ["initial", "correction1", "review1", "correction2", "review2"]; // Resolve active physical retry/hold states too. The reservation layer decides // whether they may spawn; absence must never downgrade a correction to ordinary work. const IMPLICIT_REVIEW_STATES = new Set(["allowed", "resume_reserved", "spawn_accepted", "physical_accepted", "physical_retry", "physical_exhausted", "primary_hold", "abort_hold"]); function reviewDispatchFor(agent: string, identity: Record, _task = "", secondary = false) { return !secondary && (REVIEWER_AGENTS.has(agent) || (CORRECTION_OWNERS.has(agent) && Object.values(identity).some((value) => value !== undefined))); } // Shared review-dispatch resolution for both seams: derives identity from the // task/authority when no explicit identity is supplied; both seams must resolve // identically so retry/hold authority cannot drift. function resolveReviewDispatch(params: any, suppliedIdentity: any, agent: string, task: string, lifecycle: { stateDir: string; pipelineId?: string }, project: string, secondary: boolean): { reviewDispatch: boolean; resolvedReview: { identity: Record; pipelineId: string } | undefined } { let reviewDispatch = reviewDispatchFor(agent, suppliedIdentity, task, secondary); let resolvedReview: { identity: Record; pipelineId: string } | undefined; if (!secondary && CORRECTION_OWNERS.has(agent) && !Object.values(suppliedIdentity).some((value) => value !== undefined) && extractPlanId(task) !== "unknown-plan") { resolvedReview = resolveReviewIdentity(params, lifecycle, project); reviewDispatch = Boolean(resolvedReview); } if (reviewDispatch && !resolvedReview) resolvedReview = resolveReviewIdentity(params, lifecycle, project); if (reviewDispatch && !resolvedReview) throw new Error("REVIEW_IDENTITY_INVALID"); return { reviewDispatch, resolvedReview }; } function derivedAttemptId(runFamilyId: string, reviewGate: string, reviewMode: string): string { return `attempt-${createHash("sha256").update(`${runFamilyId}|${reviewGate}|${reviewMode}`).digest("hex").slice(0, 16)}`; } function resolveHostRuntimeContext(task: string, agent: string, lifecycle: { stateDir: string }, project: string, options: HostAgentBoundaryOptions): { runtimeContext: unknown; taskContext: string } { const planId = normalizePlanKey(extractPlanId(task)); if (planId === "unknown-plan") return { runtimeContext: undefined, taskContext: "Rule runtime context: non_attested; descriptive only, not usable_for_evidence." }; let pipelineId: string; try { pipelineId = resolvePlanReviewAuthority({ stateDir: lifecycle.stateDir, project, planId }).pipelineId; } catch { return { runtimeContext: undefined, taskContext: "Rule runtime context: non_attested; lifecycle authority absent or ambiguous, not usable_for_evidence." }; } const store = openRuleLifecycleStore({ stateRoot: lifecycle.stateDir }); const scopeId = createHash("sha256").update(path.resolve(project)).digest("hex").slice(0, 24); try { const bootstrap = bootstrapRuleInventoryProjections({ store, stateRoot: lifecycle.stateDir, root: PACKAGE_ROOT, projectRoot: project, projectScopeId: scopeId, repositories: { global: PACKAGE_ROOT, project }, }); const prepared = prepareLifecycleRuntimeContext({ store, pipeline_id: pipelineId, repository: project, scope_id: scopeId, repositories: [{ repository: PACKAGE_ROOT, scope_id: null }, { repository: project, scope_id: scopeId }], authority_descriptors: bootstrap.authority_descriptors, project_authority: { project_root: project, plan_id: planId, scope_id: scopeId }, run_identity: { run_id: pipelineId, plan_id: planId, model_identity: "host-direct", config_fingerprint: "host-direct-v1", correlation_id: pipelineId }, }); const runtimeContext = store.getOrCreateRuntimeContext(pipelineId, prepared.input_digests, prepared.createRuntimeContext); const phase = lifecycleRulePhase(agent); const governedRules = renderVerifiedRuntimeRules({ stateRoot: lifecycle.stateDir, resolverSnapshot: runtimeContext.resolver_snapshot, agent, phase }); validateRequiredReviewerProducer({ agent, phase, resolverSnapshot: runtimeContext.resolver_snapshot, rendered: governedRules }); return { runtimeContext, taskContext: `${governedRules.rendered}\n\nRule runtime context: attested lifecycle authority.` }; } catch (error) { if (error instanceof Error && ["RULE_INVENTORY_BOOTSTRAP_UNAVAILABLE", "RULE_RUNTIME_CONTEXT_AUTHORITY_UNAVAILABLE", "RULE_RUNTIME_CONTEXT_CONFLICT"].includes(error.message)) { if (error.message === "RULE_RUNTIME_CONTEXT_CONFLICT") throw error; return { runtimeContext: undefined, taskContext: "Rule runtime context: non_attested; verified runtime inputs unavailable, not usable_for_evidence." }; } throw error; } finally { store.close(); } } function resolveReviewIdentity(params: any, lifecycle: { stateDir: string; pipelineId?: string }, project: string): { identity: Record; pipelineId: string } | undefined { const supplied = { runFamilyId: params?.runFamilyId, planId: params?.planId, reviewGate: params?.reviewGate, reviewMode: params?.reviewMode, attemptId: params?.attemptId }; if (Object.values(supplied).some((value) => value !== undefined)) { if (!validateReviewIdentity(supplied).ok) throw new Error("REVIEW_IDENTITY_INVALID"); if (!reviewAgentMatches(String(params?.agent), supplied)) throw new Error("REVIEW_DISPATCH_DENIED"); // A fresh host may have no ambient pipeline context. Resolve the recorded // plan authority, never invent a stream from cwd or from runFamilyId. // Supplied context stays binding: downstream mismatch checks must reject it. const pipelineId = lifecycle.pipelineId ?? resolvePlanReviewAuthority({ stateDir: lifecycle.stateDir, project, planId: supplied.planId }).pipelineId; return { identity: supplied as Record, pipelineId }; } const planId = normalizePlanKey(extractPlanId(String(params?.task || ""))); let pipelineId = ""; let rows: any[] = []; try { const authority = resolvePlanReviewAuthority({ stateDir: lifecycle.stateDir, project, planId }); pipelineId = authority.pipelineId; rows = authority.rows; } catch (error) { if (error instanceof Error && error.message === "REVIEW_AUTHORITY_NOT_FOUND") return undefined; if (error instanceof Error && error.message === "REVIEW_HISTORY_INVALID") throw new Error("REVIEW_IDENTITY_INVALID"); if (error instanceof Error && /^REVIEW_[A-Z0-9_]+$/.test(error.message)) throw error; throw Object.assign(new Error("REVIEW_LIFECYCLE_UNAVAILABLE"), { cause: error }); } const candidates = REVIEW_GATES.flatMap((reviewGate) => REVIEW_MODES.map((reviewMode) => ({ runFamilyId: pipelineId, planId, reviewGate, reviewMode, attemptId: derivedAttemptId(pipelineId, reviewGate, reviewMode) }))) .filter((identity) => { if (!reviewAgentMatches(String(params?.agent), identity)) return false; const state = foldReviewHistory(rows, identity); return IMPLICIT_REVIEW_STATES.has(state.status) || (state.status === "uncertain" && rows.some((row) => ["runFamilyId", "planId", "reviewGate", "reviewMode", "attemptId"].every((key) => row?.metadata?.[key] === identity[key]))); }); if (candidates.length > 1) throw new Error("REVIEW_IDENTITY_INVALID"); if (candidates.length === 0 && CORRECTION_OWNERS.has(String(params?.agent))) { // A partial/corrupt completion or advanced pending identity is not ordinary // work. This matters when a process dies between durable completion rows. const gates = params.agent === "pidex-planner" ? ["critic"] : ["code-review", "security", "qa"]; for (const gate of gates) { const history = rows.filter(row => row?.metadata?.planId === planId && row.metadata.reviewGate === gate); if (!history.length) continue; const actual = history.map(row => validateReviewIdentity(row.metadata)).find(value => value.ok)?.value; if (!actual) throw new Error("REVIEW_HISTORY_INVALID"); const state = foldReviewHistory(rows, actual); if (state.status !== "terminal" || state.terminal !== "accepted") throw new Error("REVIEW_HISTORY_INVALID"); } } return candidates.length === 1 ? { identity: candidates[0], pipelineId } : undefined; } function isRelativeAgentsOutputPath(value: unknown): value is string { if (typeof value !== "string" || !value || path.isAbsolute(value) || path.win32.isAbsolute(value) || value.includes("\\")) return false; return path.posix.normalize(value) === value && /^agents\.output\/.+/.test(value); } function isStrictDescendant(parent: string, child: string): boolean { const relative = path.relative(parent, child); return Boolean(relative) && relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative); } function canonicalExistingPath(root: string, relative: string, regularFile = false): string | undefined { try { const canonicalRoot = fs.realpathSync.native(root); const candidate = path.resolve(canonicalRoot, relative); if (!isStrictDescendant(canonicalRoot, candidate)) return undefined; let current = canonicalRoot; for (const part of path.relative(canonicalRoot, candidate).split(path.sep)) { current = path.join(current, part); if (fs.lstatSync(current).isSymbolicLink()) return undefined; } const canonical = fs.realpathSync.native(candidate); return canonical === candidate && (!regularFile || fs.statSync(canonical).isFile()) ? canonical : undefined; } catch { return undefined; } } function isHostReviewContext(project: string, contextFile: unknown): boolean { if (!isRelativeAgentsOutputPath(contextFile)) return false; try { const canonicalProject = fs.realpathSync.native(project); const agentsOutput = canonicalExistingPath(canonicalProject, "agents.output"); const context = canonicalExistingPath(canonicalProject, contextFile, true); return Boolean(agentsOutput && fs.statSync(agentsOutput).isDirectory() && context && isStrictDescendant(agentsOutput, context)); } catch { return false; } } function isDirectReviewContext(params: any, result: any, lifecycle: { stateDir: string }, expectedContextFile: string, contextFile: unknown): boolean { if (!isRelativeAgentsOutputPath(expectedContextFile) || contextFile !== expectedContextFile || result.context_file !== expectedContextFile || result.archive_sync_status !== "complete" || typeof result.archive_context_file !== "string" || typeof params?.projectId !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(params.projectId)) return false; try { const canonicalState = fs.realpathSync.native(lifecycle.stateDir); const expectedArchive = canonicalExistingPath(canonicalState, path.join("project-archives", params.projectId, ...expectedContextFile.split("/")), true); const suppliedArchive = path.resolve(result.archive_context_file); if (!expectedArchive || !isStrictDescendant(canonicalState, suppliedArchive)) return false; return canonicalExistingPath(canonicalState, path.relative(canonicalState, suppliedArchive), true) === expectedArchive; } catch { return false; } } function completeReviewDispatch(agent: string, identity: Record, result: any, lifecycle: { stateDir: string; pipelineId: string; project: string; archiveAuthority?: { pidexRoot: string; projectId: string; lockTimeoutMs?: number } }, cwd: string, expectedContextFile?: string, params?: any, structuredRequired = false): any { if (!result || (result.exitCode ?? (result.ok === true ? 0 : 1)) !== 0) throw new Error("REVIEW_CHILD_FAILED"); const routing = extractRoutingBlock(String(result.finalText || "")); const verdict = extractRoutingField(routing, "verdict") ?? result.routing?.verdict; const routeTo = extractRoutingField(routing, "route_to") ?? result.routing?.route_to; const contextFile = extractRoutingField(routing, "context_file") ?? result.routing?.context_file; const localContext = expectedContextFile ? isDirectReviewContext(params, result, lifecycle, expectedContextFile, contextFile) : isHostReviewContext(cwd, contextFile); if ((!routing && !result.routing) || !verdict || !routeTo || !localContext) throw new Error("REVIEW_ROUTING_INVALID"); // Plan 059 Slice 1: host-direct primary reviews complete through the canonical // structured boundary, which reads the exact assigned artifact, enforces the // pidex-review-outcome-v1 contract and terminal matrix, and agrees the ROUTING // verdict. Prompt-only classification is never load-bearing. Corrections carry // no structured payload and Project Pipeline adoption is Slice 3; both keep the // legacy ROUTING path. if (structuredRequired && !identity.reviewMode.startsWith("correction")) { // Plan 059 Slice 3 (req 1/6): Project Pipeline completion passes the freshly // reloaded registry-derived archive option when the registered authority kind // is archive-only, so the boundary re-derives and re-validates the archive // root before any write and holds the external archive lock across the TBR // write + lifecycle completion (TBR -> archive -> selection -> gate). const completion = completeStructuredReviewOutcome({ stateDir: lifecycle.stateDir, project: lifecycle.project, pipelineId: lifecycle.pipelineId, identity: identity as Record, artifactPath: contextFile as string, routingVerdict: verdict, routeTo, archive: lifecycle.archiveAuthority ? { pidexRoot: lifecycle.archiveAuthority.pidexRoot, projectId: lifecycle.archiveAuthority.projectId, lockTimeoutMs: lifecycle.archiveAuthority.lockTimeoutMs } : undefined, }); // Plan 059 Slice 2 (item 7): typed completion status surfaced on the host boundary // result — accepted | CHANGES_REQUESTED | USER_DECISION_REQUIRED | CLOSED_WITH_TBR | // resumed — so the Slice 4 policy consumer can distinguish normal correction, // user-decision-required expansion, approval, and terminal TBR close without // re-deriving truth from prose. SKILL policy itself is untouched in this slice. if (["accepted", "CHANGES_REQUESTED", "USER_DECISION_REQUIRED", "CLOSED_WITH_TBR", "resumed"].includes(completion.status)) return { ...result, reviewCompletion: completion }; if (completion.status === "TBR_WRITE_BLOCKED") throw new Error("TBR_WRITE_BLOCKED"); if (completion.status === "denied") throw new Error(completion.code || "REVIEW_OUTCOME_INVALID"); if (completion.status === "unavailable") throw new Error(completion.code || "REVIEW_LIFECYCLE_UNAVAILABLE"); throw new Error("REVIEW_COMPLETION_UNAVAILABLE"); } const outcome = identity.reviewMode.startsWith("correction") ? verdict === "COMPLETE" && routeTo === ({ critic: "pidex-critic", "code-review": "pidex-code-reviewer", security: "pidex-security", qa: "pidex-qa" }[identity.reviewGate]) ? "READY_FOR_REVIEW" : null : normalizeReviewVerdict(identity.reviewGate, verdict); if (!outcome) throw new Error("REVIEW_OUTCOME_INVALID"); const completion = recordReviewCompletion({ ...lifecycle, identity, outcome }); if (completion.status === "TBR_WRITE_BLOCKED") throw new Error("TBR_WRITE_BLOCKED"); if (completion.status !== outcome && completion.status !== "resumed") throw new Error(completion.code || "REVIEW_COMPLETION_UNAVAILABLE"); return result; } // Shared physical retry/hold dispatch for both review seams (host async + Project // Pipeline sync): resume validation, reservation status fan-out, physical attempt // derivation, and child-failure classification stay identical across seams so // retry authority and typed hold outcomes cannot drift between them. function reviewResumeFrom(params: any, extraInvalid?: boolean): { resumeHoldId: string; resumeConfirmed: boolean } | undefined { const resumeFieldsSupplied = params.resumeHoldId !== undefined || params.resumeConfirmed !== undefined; if (resumeFieldsSupplied && (extraInvalid || params.resumeConfirmed !== true || typeof params.resumeHoldId !== "string" || !/^hold-[a-f0-9]{32}$/.test(params.resumeHoldId))) throw new Error("REVIEW_RESUME_INVALID"); return resumeFieldsSupplied ? { resumeHoldId: params.resumeHoldId, resumeConfirmed: params.resumeConfirmed } : undefined; } // Fan-out on a resolved reservation: held/uncertain/retryable/exhausted return // typed seam results; accepted returns null so the caller dispatches the child. function reviewReservationStatus(identity: Record, reservation: any, held: (reviewCompletion: any) => any, uncertain: () => any, exhausted: (physical: any) => any): { retry: number } | { done: any } | null { if (reservation.status === "held") return { done: held(reservation.reviewCompletion) }; if (reservation.status === "uncertain") return { done: uncertain() }; if (reservation.status === "retryable") return { retry: true, generation: reservation.physicalGeneration }; if (reservation.status === "exhausted") { const exhaustedPhysical = deriveReviewPhysicalAttempt(identity, reservation.physicalGeneration, reservation.physicalOrdinal); if (!exhaustedPhysical) throw new Error("REVIEW_PHYSICAL_ATTEMPT_INVALID"); return { done: exhausted(exhaustedPhysical) }; } if (reservation.status !== "accepted") throw new Error(reservation.status === "resumed" ? "REVIEW_DISPATCH_RESUMED" : reservation.code || "REVIEW_DISPATCH_DENIED"); return null; } function reviewPhysicalOf(identity: Record, reservation: any, requestedPhysical: any): any { const physical = reservation.physical ?? (Number.isInteger(reservation.physicalGeneration) && Number.isInteger(reservation.physicalOrdinal) ? deriveReviewPhysicalAttempt(identity, reservation.physicalGeneration, reservation.physicalOrdinal) : requestedPhysical); if (!physical) throw new Error("REVIEW_PHYSICAL_ATTEMPT_INVALID"); return physical; } // Shared child-failure classification (SEC-2): setup/auth/config/identity/sandbox/ // write-fence denials are deterministic and must never consume the automatic // physical retry; only typed child/routing failures classify. Typed hold control // results stop the seam instead of retrying (AC-62-15). Same evidence contract // for both seams. function reviewChildFailure(lifecycle: any, identity: Record, physical: any, physicalOrdinal: number, code: string, result: any, artifactPresentOf: (result: any) => boolean): { rethrow: true } | { retry: number } | { done: any } { if (!['REVIEW_CHILD_FAILED', 'REVIEW_ROUTING_INVALID', 'REVIEW_OUTCOME_INVALID'].includes(code)) return { rethrow: true }; if (code === 'REVIEW_OUTCOME_INVALID') return { rethrow: true }; if (result?.aborted) return { done: { ...result, reviewCompletion: recordReviewAbortHold({ ...lifecycle, identity, physical }) } }; if (result?.setupError) throw new Error("REVIEW_SETUP_DENIED"); const outcome = code === 'REVIEW_ROUTING_INVALID' ? 'MALFORMED_COMPLETION' : result?.timedOut ? 'TIMED_OUT' : result?.turnLimitHit ? 'TURN_LIMIT_HIT' : 'FAILED_TO_RUN'; const recorded = recordReviewPhysicalOutcome({ ...lifecycle, identity, physical, outcome, evidence: { exitCode: result?.exitCode, timedOut: result?.timedOut, turnLimitHit: result?.turnLimitHit, finalTextPresent: Boolean(result?.finalText), routingPresent: Boolean(extractRoutingBlock(String(result?.finalText || ''))), artifactPresent: artifactPresentOf(result) } }); if (recorded.status !== 'retryable' && recorded.status !== 'exhausted') throw new Error(recorded.code || 'REVIEW_LIFECYCLE_UNAVAILABLE'); if (physicalOrdinal === 1) return { done: { ...result, reviewCompletion: recordReviewHold({ ...lifecycle, identity, physical, status: 'PRIMARY_REVIEW_UNAVAILABLE' }) } }; return { retry: true, generation: physical.physicalGeneration }; } export async function executeHostAgentBoundary(params: HostAgentRequest & { task: string; tools?: string[] }, options: HostAgentBoundaryOptions): Promise { validateHostAgentRequestShape(params); if (params.closeout) { validateCloseoutRequest(params.closeout); if ([params.laneId, params.resumeHoldId, params.resumeConfirmed, (params as any).reviewIdentity, (params as any).projectId, (params as any).expectedInputPath, (params as any).expectedOutputPath, params.runFamilyId, params.planId, params.reviewGate, params.reviewMode, params.attemptId].some(v => v !== undefined)) throw new Error("PIPELINE_CLOSEOUT_REQUEST_INVALID"); params = { ...params, task: `Plan: ${params.closeout.planId.slice(5)}\n${params.task}` }; } assertRuntimeBaseline(options.agentProjectMode?.mode ?? "host-direct"); const request = validateHostAgentRequestShape(params); const suppliedIdentity = { runFamilyId: params.runFamilyId, planId: params.planId, reviewGate: params.reviewGate, reviewMode: params.reviewMode, attemptId: params.attemptId }; const configuredLifecycle = options.reviewLifecycle ?? { stateDir: STATE_DIR, pipelineId: process.env.RUNNING_PI_PIPELINE_ID || process.env.PIDEX_PIPELINE_ID || undefined }; const { reviewDispatch, resolvedReview } = resolveReviewDispatch(params, suppliedIdentity, params.agent, params.task, configuredLifecycle, options.agentCwd, request.secondary); const identity = resolvedReview?.identity ?? suppliedIdentity; const eligibleLanes = request.secondary ? (options.loadEligibleLanes ?? ((requestParams) => { const statusScript = path.join(PACKAGE_ROOT, "modules", "pidex", "parallel-agents", "scripts", "status.mjs"); const status = spawnSync(process.execPath, [statusScript, "--root", PACKAGE_ROOT, "eligible", "--agent", String(requestParams.agent), "--trigger", String(requestParams.trigger), "--json"], { cwd: PACKAGE_ROOT, encoding: "utf8", timeout: 10_000 }); if (status.status !== 0) throw new Error("Host pidex_agent could not resolve configured parallel lanes before spawn."); try { const parsed = JSON.parse(status.stdout || "{}"); return Array.isArray(parsed?.lanes) ? parsed.lanes : []; } catch { throw new Error("Host pidex_agent received invalid configured parallel lane data before spawn."); } }))(params) : []; const route = resolveHostAgentRoute(params, (options.loadConfig ?? loadRoutingConfig)(), eligibleLanes); const sandboxState = (options.resolveSandboxState ?? (() => resolveSandboxStateForProjectMode(options.agentProjectMode)))(); if (params.closeout && (process.platform !== "linux" || sandboxState.enabled || request.secondary || reviewDispatch || !isPiProvider(route.provider) || (options.agentProjectMode?.mode && options.agentProjectMode.mode !== "host-direct"))) throw new Error("PIPELINE_CLOSEOUT_RECOVERY_UNCOVERED"); if (sandboxState.enabled) { const probe = (options.probeSandbox ?? probeSandboxAvailability)(); if (!probe.ok) throw new Error(`PIDEX sandbox is enabled but unavailable: ${probe.summary}`); } const sandboxTaskPrefix = sandboxState.enabled ? [ "SANDBOX ACTIVE: hardened-pipeline is enabled for this PIDEX run.", "Sandbox is internal hardening; preserve normal specialist behavior and ROUTING.", "Do not ask the user mid-run for sandbox configuration. If sandbox evidence cannot be produced, fail with an actionable reason.", "Use sandbox runtime helpers for project commands when relevant. Do not read env/secret/runtime paths. Do not write host project paths outside the assigned workspace.", "Include sandbox evidence or SANDBOX-SKIP in your artifact.", "", ].join("\n") : ""; const hostRuntime = resolveHostRuntimeContext(params.task, params.agent, configuredLifecycle, options.agentCwd, options); const runParams = { agent: params.agent, task: `${sandboxTaskPrefix}${params.task}\n\n${hostRuntime.taskContext}`, runtimeContext: hostRuntime.runtimeContext, cwd: options.agentCwd, route, tools: params.tools, signal: options.signal, onUpdate: options.onUpdate, }; const runner = sandboxState.enabled && SANDBOXED_AGENT_NAMES.has(params.agent) ? (options.runSandboxed ?? runSandboxedConfiguredAgent) : (options.runConfigured ?? runConfiguredAgent); if (!reviewDispatch && params.closeout) { if (options.signal?.aborted) throw new Error("PIPELINE_CLOSEOUT_ABORT_HOLD"); assertExecutionSupport(); const definition = loadAgent(params.agent); const definitionDigest = executionValueDigest(definition); const systemPromptDigest = executionValueDigest(buildAgentSystemPrompt(params.agent, definition.body, "pi")); const scope = executionValueDigest({ protocol: EXECUTION_PROTOCOL_DIGEST, root: PACKAGE_ROOT, route, tools: params.tools, definitionDigest, systemPromptDigest, rules: hostRuntime.taskContext, learning: "deferred-no-model-replay" }); const ticket = beginRecoverableHostCloseout({ stateDir: configuredLifecycle.stateDir, project: options.agentCwd, actor: params.agent, request: params.closeout, scope }); const abort = () => { try { ticket.abort(); } catch { /* no success after signal; process stop still must run */ } }; options.signal?.addEventListener("abort", abort, { once: true }); try { if (params.closeout.action === "start") { const result = await runner({ ...runParams, task: runParams.task + ticket.instruction, closeoutExecution: { stateRoot: configuredLifecycle.stateDir, binding: ticket.executionBinding, definitionDigest, systemPromptDigest }, onProcessStarted: ticket.pinStart, onCloseoutReturned: ticket.capture }); if (!ticket.isCaptured()) ticket.capture(result); } if (options.signal?.aborted) throw new Error("PIPELINE_CLOSEOUT_ABORT_HOLD"); // Explicit opt-in recovery defers automatic rule learning. Replay never // launches a hidden coordinator/model or adopts configuration changes. return await ticket.complete(async () => "deferred") as RpResult; } catch (error: any) { throw new Error(`${error?.message ?? error}; closeout_dispatch=${ticket.id}; no automatic retry`); } finally { options.signal?.removeEventListener("abort", abort); } } if (!reviewDispatch) { const closeout = process.platform === "linux" && !sandboxState.enabled && !request.secondary ? beginHostCloseoutDispatch({ stateDir: configuredLifecycle.stateDir, project: options.agentCwd, planId: normalizePlanKey(extractPlanId(params.task)), actor: params.agent }) : null; const result = await runner({ ...runParams, task: runParams.task + (closeout?.instruction ?? "") }); if (params.agent === "pidex-retrospective") { (result as any).automatic_learning = await runHostRetrospectiveAutomaticLearning({ result, agentCwd: options.agentCwd, source: options.automaticRetrospectiveSource?.() }); } if (closeout) (result as any).closeoutCompletion = closeout.finish(result); return result; } if (options.signal?.aborted) throw new Error("REVIEW_DISPATCH_ABORTED"); const lifecycle = { ...configuredLifecycle, pipelineId: resolvedReview!.pipelineId, project: options.agentCwd }; const hostHeld = (reviewCompletion: any) => ({ agent: params.agent, provider: route.provider, exitCode: 0, stderr: "", finalText: "", reviewCompletion }); let executionScope: { scope: string; definitionDigest: string; systemPromptDigest: string } | undefined; if (process.platform === "linux" && !sandboxState.enabled && isPiProvider(route.provider) && runner === runConfiguredAgent && fs.realpathSync(options.agentCwd) === options.agentCwd) { assertExecutionSupport(); const definition = loadAgent(params.agent); const definitionDigest = executionValueDigest(definition); const systemPromptDigest = executionValueDigest(buildAgentSystemPrompt(params.agent, definition.body, "pi")); executionScope = { definitionDigest, systemPromptDigest, scope: executionValueDigest({ protocol: EXECUTION_PROTOCOL_DIGEST, root: PACKAGE_ROOT, route, tools: params.tools, definitionDigest, systemPromptDigest, rules: hostRuntime.taskContext }) }; const recovered = reconcileReviewExecution({ ...lifecycle, identity, actor: params.agent, scope: executionScope.scope }); if (recovered.status === "held") return hostHeld(recovered.reviewCompletion); if (["running", "uncertain"].includes(recovered.status)) return hostHeld({ status: "REVIEW_DISPATCH_UNCERTAIN", executionState: recovered.status, code: recovered.code }); if (!["not_pending", "reconciled"].includes(recovered.status)) throw new Error(recovered.code || "REVIEW_EXECUTION_UNAVAILABLE"); } let physicalGeneration = 0; let resume = reviewResumeFrom(params, request.secondary); for (const physicalOrdinal of [0, 1]) { const requestedPhysical = resume ? undefined : deriveReviewPhysicalAttempt(identity, physicalGeneration, physicalOrdinal); if (!resume && !requestedPhysical) throw new Error("REVIEW_PHYSICAL_ATTEMPT_INVALID"); const reservation = await reserveReviewStartAsync({ ...lifecycle, identity, physical: requestedPhysical, resume, start: (onProcessStarted, physical) => runner({ ...runParams, onProcessStarted, reviewDispatch: true, reviewExecution: executionScope ? { stateRoot: lifecycle.stateDir, definitionDigest: executionScope.definitionDigest, systemPromptDigest: executionScope.systemPromptDigest, binding: { project: options.agentCwd, pipelineId: lifecycle.pipelineId, identity, physical, actor: params.agent, scope: executionScope.scope } } : undefined }) }); const status = reviewReservationStatus(identity, reservation, (c) => hostHeld(c), () => hostHeld({ status: "REVIEW_DISPATCH_UNCERTAIN" }), (p) => hostHeld(recordReviewHold({ ...lifecycle, identity, physical: p, status: "PRIMARY_REVIEW_UNAVAILABLE" }))); if (status?.retry) { physicalGeneration = status.generation; resume = undefined; continue; } if (status?.done) return status.done; const physical = reviewPhysicalOf(identity, reservation, requestedPhysical); try { return completeReviewDispatch(params.agent, identity as Record, await reservation.started, lifecycle, options.agentCwd, undefined, params, true); } catch (error: any) { const failure = reviewChildFailure(lifecycle, identity, physical, physicalOrdinal, String(error?.message || error), await reservation.started, (r) => { const reportedContext = extractRoutingField(extractRoutingBlock(String(r?.finalText || '')), 'context_file'); return isRelativeAgentsOutputPath(reportedContext) && fs.existsSync(path.join(options.agentCwd, ...reportedContext.split('/'))); }); if (failure.rethrow) throw error; if (failure.retry) { physicalGeneration = failure.generation; resume = undefined; continue; } return failure.done; } } throw new Error("REVIEW_DISPATCH_DENIED"); } const PUBLIC_REVIEW_IDENTITY_KEYS = ["runFamilyId", "planId", "reviewGate", "reviewMode", "attemptId"].sort(); export function normalizePublicReviewIdentity(params: any): any { if (params?.reviewIdentity === undefined) return params; const flat = Object.fromEntries(PUBLIC_REVIEW_IDENTITY_KEYS.map((key) => [key, params?.[key]])); const nested = params.reviewIdentity; if (Object.values(flat).some((value) => value !== undefined) || !nested || typeof nested !== "object" || Array.isArray(nested) || JSON.stringify(Object.keys(nested).sort()) !== JSON.stringify(PUBLIC_REVIEW_IDENTITY_KEYS) || !validateReviewIdentity(nested).ok) throw new Error("REVIEW_IDENTITY_INVALID"); const { reviewIdentity: _reviewIdentity, ...rest } = params; return { ...rest, ...nested }; } export const PidexAgentParams = Type.Object({ agent: Type.String({ description: "pidex-* agent to run, e.g. pidex-planner, pidex-critic, pidex-implementer" }), task: Type.String({ description: "Full task/context for the agent. Include relevant doc paths and required output path." }), cwd: Type.Optional(Type.String({ description: "Project working directory. Defaults to current Pi cwd." })), closeout: Type.Optional(Type.Object({ action: Type.Union([Type.Literal("start"), Type.Literal("resume")]), planId: Type.String(), pipelineId: Type.String(), artifactPath: Type.String(), dispatchId: Type.Optional(Type.String()), }, { additionalProperties: false, description: "Opt-in Linux primary Pi closeout receipt/replay. Explicit canonical plan/pipeline and relative agents.output artifact. resume requires original dispatchId and never starts a model. Automatic rule learning is explicitly deferred; no automatic retry or waiver." })), projectId: Type.Optional(Type.String({ description: "Explicit Project Pipeline registry project id. Required for direct project-pipeline agent calls." })), expectedInputPath: Type.Optional(Type.String({ description: "Exact container-relative agents.output/** input artifact for Project Pipeline review calls." })), expectedOutputPath: Type.Optional(Type.String({ description: "Exact container-relative agents.output/** assigned output artifact for Project Pipeline calls." })), laneId: Type.Optional(Type.String({ description: "Configured secondary lane identity. Requires exact trigger; resolves runner route from eligible parallel-lane status." })), trigger: Type.Optional(Type.String({ description: "Configured secondary lane trigger. Required with laneId." })), provider: Type.Optional(Type.String({ description: "Rejected for host-direct and hardened-pipeline calls. Routes resolve from configured primary or laneId." })), model: Type.Optional(Type.String({ description: "Rejected for host-direct and hardened-pipeline calls. Routes resolve from configured primary or laneId." })), effort: Type.Optional(Type.String({ description: "Rejected for host-direct and hardened-pipeline calls. Routes resolve from configured primary or laneId." })), tools: Type.Optional(Type.Array(Type.String(), { description: "Optional Pi tool allowlist override (only used by provider=pi/subagent)." })), resumeHoldId: Type.Optional(Type.String({ description: "Trusted-orchestrator single-use Primary hold ID. Requires resumeConfirmed:true." })), resumeConfirmed: Type.Optional(Type.Boolean({ description: "Literal true only. Trusted orchestrator confirms intervening affirmative user response." })), reviewIdentity: Type.Optional(Type.Object({ runFamilyId: Type.String(), planId: Type.String(), reviewGate: Type.Union([Type.Literal("critic"), Type.Literal("code-review"), Type.Literal("security"), Type.Literal("qa")]), reviewMode: Type.Union([Type.Literal("initial"), Type.Literal("correction1"), Type.Literal("review1"), Type.Literal("correction2"), Type.Literal("review2")]), attemptId: Type.String(), }, { additionalProperties: false, description: "Advanced explicit review identity. Omit for normal calls; all five fields are required when supplied." })), }, { additionalProperties: false }); const PROJECT_PIPELINE_REVIEW_AGENTS = new Set(["pidex-critic", "pidex-code-reviewer"]); type ProjectPipelineReviewLifecycle = { stateDir: string; pipelineId: string; project: string; projectId?: string; archiveAuthority?: { pidexRoot: string; projectId: string; lockTimeoutMs?: number }; resolveCurrentProject?: () => string }; export function executeProjectPipelineReviewBoundary(params: any, lifecycle: ProjectPipelineReviewLifecycle, start: () => any): any { const suppliedIdentity = { runFamilyId: params?.runFamilyId, planId: params?.planId, reviewGate: params?.reviewGate, reviewMode: params?.reviewMode, attemptId: params?.attemptId }; const { reviewDispatch, resolvedReview } = resolveReviewDispatch(params, suppliedIdentity, String(params?.agent || ""), String(params?.task || ""), lifecycle, lifecycle.project, false); if (!reviewDispatch) return start(); const identity = resolvedReview!.identity; const effectiveLifecycle = { ...lifecycle, pipelineId: resolvedReview.pipelineId }; const pipelineHeld = (reviewCompletion: any) => ({ exitCode: 0, reviewCompletion }); let physicalGeneration = 0; let resume = reviewResumeFrom(params); for (const physicalOrdinal of [0, 1]) { const requestedPhysical = resume ? undefined : deriveReviewPhysicalAttempt(identity, physicalGeneration, physicalOrdinal); if (!resume && !requestedPhysical) throw new Error("REVIEW_PHYSICAL_ATTEMPT_INVALID"); const reservation = reserveReviewStart({ ...effectiveLifecycle, identity, physical: requestedPhysical, resume, start }); const status = reviewReservationStatus(identity, reservation, (c) => pipelineHeld(c), () => pipelineHeld({ status: "REVIEW_DISPATCH_UNCERTAIN" }), (p) => pipelineHeld(recordReviewHold({ ...effectiveLifecycle, identity, physical: p, status: "PRIMARY_REVIEW_UNAVAILABLE" }))); if (status?.retry) { physicalGeneration = status.generation; resume = undefined; continue; } if (status?.done) return status.done; if (lifecycle.projectId) { if (!lifecycle.resolveCurrentProject) throw new Error("REVIEW_PROJECT_AUTHORITY_CHANGED"); let currentProject: string; try { currentProject = lifecycle.resolveCurrentProject(); } catch { throw new Error("REVIEW_PROJECT_AUTHORITY_CHANGED"); } if (currentProject !== lifecycle.project) throw new Error("REVIEW_PROJECT_AUTHORITY_CHANGED"); } const physical = reviewPhysicalOf(identity, reservation, requestedPhysical); try { return completeReviewDispatch(String(params.agent), identity, reservation.started, effectiveLifecycle, lifecycle.project, params?.expectedOutputPath, params, true); } catch (error: any) { const failure = reviewChildFailure(effectiveLifecycle, identity, physical, physicalOrdinal, String(error?.message || error), reservation.started, () => { const expectedArtifact = String(params?.expectedOutputPath || ''); return isRelativeAgentsOutputPath(expectedArtifact) && fs.existsSync(path.join(lifecycle.project, ...expectedArtifact.split('/'))); }); if (failure.rethrow) throw error; if (failure.retry) { physicalGeneration = failure.generation; resume = undefined; continue; } return failure.done; } } throw new Error("REVIEW_DISPATCH_DENIED"); } export function resolveProjectPipelineAuthority(projectId: string): { root: string; kind: string } { if (!fs.existsSync(PROJECT_PIPELINE_LIFECYCLE_SCRIPT)) throw new Error("Project Pipeline authority helper missing; no host fallback was used."); const proc = spawnSync(process.execPath, [PROJECT_PIPELINE_LIFECYCLE_SCRIPT, "resolve-authority", "--pidex-root", PACKAGE_ROOT, "--project-id", projectId, "--json"], { cwd: PACKAGE_ROOT, encoding: "utf8", timeout: 30_000, maxBuffer: 1024 * 1024 }); let parsed: any; try { parsed = JSON.parse(proc.stdout || "{}"); } catch { throw new Error("Project Pipeline authority resolution failed; no host fallback was used."); } if (proc.status !== 0 || parsed?.ok !== true || typeof parsed?.project_root !== "string" || !path.isAbsolute(parsed.project_root) || !["host-path", "archive"].includes(String(parsed?.authority_kind))) throw new Error("Project Pipeline authority resolution failed; no host fallback was used."); let canonical: string; try { canonical = fs.realpathSync.native(parsed.project_root); } catch { throw new Error("Project Pipeline authority resolution failed; no host fallback was used."); } if (canonical !== parsed.project_root) throw new Error("Project Pipeline authority resolution changed; no host fallback was used."); return { root: canonical, kind: String(parsed.authority_kind) }; } export function resolveProjectPipelineAuthorityRoot(projectId: string): string { return resolveProjectPipelineAuthority(projectId).root; } function validateProjectPipelineAgentParams(params: any): void { if (params?.closeout !== undefined) throw new Error("PIPELINE_CLOSEOUT_RECOVERY_UNCOVERED"); if (params?.provider !== undefined || params?.model !== undefined || params?.effort !== undefined) throw new Error("Direct Project Pipeline pidex_agent calls reject caller-supplied provider, model, or effort."); if (!params?.projectId) throw new Error("Direct Project Pipeline pidex_agent calls require projectId; no host fallback was used."); if (!isRelativeAgentsOutputPath(params?.expectedOutputPath)) throw new Error("Direct Project Pipeline pidex_agent calls require expectedOutputPath under agents.output/**."); if (PROJECT_PIPELINE_REVIEW_AGENTS.has(String(params.agent)) && !isRelativeAgentsOutputPath(params?.expectedInputPath)) throw new Error(`Direct Project Pipeline review '${params.agent}' requires expectedInputPath.`); } export function runProjectPipelineAgentTool(params: any): any { validateProjectPipelineAgentParams(params); assertRuntimeBaseline("project-pipeline"); const suppliedIdentity = [params?.runFamilyId, params?.planId, params?.reviewGate, params?.reviewMode, params?.attemptId].some((value) => value !== undefined); const trackedCandidate = PROJECT_PIPELINE_REVIEW_AGENTS.has(String(params?.agent || "")) || (CORRECTION_OWNERS.has(String(params?.agent || "")) && (suppliedIdentity || extractPlanId(String(params?.task || "")) !== "unknown-plan")); const authority = trackedCandidate ? resolveProjectPipelineAuthority(String(params.projectId)) : undefined; const project = authority?.root ?? path.resolve(params?.cwd || PACKAGE_ROOT); const projectId = String(params.projectId); // Plan 059 Slice 3 (req 1/6): the registered authority is reloaded per call // (resolveProjectPipelineAuthority re-derives host-path vs registry-derived // archive root from the current project record — no cwd/custom archive/URL // fallback). Archive-only authority carries the fresh archive option into the // completion boundary, which re-derives and re-validates it before any write. const lifecycle: ProjectPipelineReviewLifecycle = { stateDir: STATE_DIR, pipelineId: process.env.RUNNING_PI_PIPELINE_ID || process.env.PIDEX_PIPELINE_ID || `${projectId}-${params?.planId || "plan"}`, project, projectId, ...(authority?.kind === "archive" ? { archiveAuthority: { pidexRoot: PACKAGE_ROOT, projectId } } : {}), resolveCurrentProject: () => resolveProjectPipelineAuthorityRoot(projectId), }; return executeProjectPipelineReviewBoundary(params, lifecycle, () => runProjectPipelineAgentToolUnchecked(params)); } function runProjectPipelineAgentToolUnchecked(params: any): any { validateProjectPipelineAgentParams(params); if (!fs.existsSync(PROJECT_PIPELINE_RUN_AGENT_SCRIPT)) throw new Error("Project Pipeline agent helper missing; update the canonical PIDEX runtime."); const args = [PROJECT_PIPELINE_RUN_AGENT_SCRIPT, "--pidex-root", PACKAGE_ROOT, "--project-id", String(params.projectId), "--agent", String(params.agent), "--task", String(params.task || ""), "--expected-output", String(params.expectedOutputPath), "--json"]; if (params.expectedInputPath) args.push("--expected-input", String(params.expectedInputPath)); if (params.reviewGate) args.push("--review-gate", String(params.reviewGate)); if (params.reviewMode) args.push("--review-mode", String(params.reviewMode)); if (PROJECT_PIPELINE_REVIEW_AGENTS.has(String(params.agent))) args.push("--review-write-fence"); const proc = spawnSync(process.execPath, args, { cwd: PACKAGE_ROOT, encoding: "utf8", timeout: 30 * 60_000, maxBuffer: 20 * 1024 * 1024 }); let result: any; try { result = JSON.parse(proc.stdout || "{}"); } catch { throw new Error(`Project Pipeline agent helper returned invalid JSON (exit=${proc.status ?? 1}); no host fallback was used.`); } if (proc.status !== 0 || result?.ok !== true) { const error = String(result?.error || "agent-run-failed"); if (error === "module-rule-injection-failed") throw new Error("Project Pipeline agent failed: module-rule-injection-failed; no host fallback was used."); throw new Error(`Project Pipeline pidex_agent '${params.agent}' failed: ${error}${result?.reason ? ` (${String(result.reason).slice(0, 500)})` : ""}; no host fallback was used.`); } return result; } export default function runningPi(pi: ExtensionAPI) { pi.on("tool_call", async (event: any, ctx: any) => { const sandboxBlock = inspectSandboxToolCall(event, ctx); if (sandboxBlock) return sandboxBlock; const boundaryBlock = inspectProjectBoundaryToolCall(event, ctx); if (boundaryBlock) return boundaryBlock; if (process.env[PIDEX_CHILD_ENV] === "1") return undefined; const sandboxContext = readSandboxRuntimeContext(); if (sandboxContext && event?.toolName === "read") { const blocked = sensitiveSandboxPath(event?.input?.path || event?.input?.file || event?.input?.filepath); if (blocked) return { block: true, reason: `PIDEX sandbox blocks reads of env/secret/runtime paths while active: ${blocked}` }; } if (event?.toolName !== "bash") return undefined; const command = event?.input?.command; if (typeof command !== "string") return undefined; const risk = inspectBashForGitHookRisk(command); if (!risk) return undefined; if (risk.block) return { block: true, reason: risk.block }; if (risk.warn) { if (!ctx.hasUI) return { block: true, reason: "PIDEX Git hook protection change requires interactive approval." }; const choice = await ctx.ui.select(`${risk.warn}\n\nCommand:\n${command.slice(0, 1200)}\n\nAllow?`, ["Yes", "No"]); if (choice !== "Yes") return { block: true, reason: "PIDEX Git hook protection change was not approved." }; } return undefined; }); if (process.env[PIDEX_CHILD_ENV] === "1") return; registerRuntimeBaseline(pi, { bootstrapRoot: BOOTSTRAP_ROOT, runtimeRoot: PACKAGE_ROOT, stateRoot: STATE_DIR }, PI_VERSION); pi.registerCommand("pidexaudit", { description: "Audit pidex context usage from metrics + child logs.", handler: async (argLine, ctx) => { const homeStatus = canonicalHomeStatus(); if (!homeStatus.ok) { await ctx.ui.notify(homeStatus.message?.includes("not a valid") ? `${homeStatus.message}\nMove or repair that directory before running pidexaudit.` : canonicalHomeMissingMessage(), "warning"); return; } const parsed = parseRpAuditOptions(argLine); if (parsed.help) { await ctx.ui.notify(rpAuditUsage(), "info"); return; } if (parsed.error || !parsed.options) { await ctx.ui.notify(`${parsed.error ?? "Invalid args"}\n\n${rpAuditUsage()}`, "warning"); return; } try { const result = runRpAudit(ctx.cwd, parsed.options); await ctx.ui.notify(result.summary, "info"); } catch (error: any) { await ctx.ui.notify(`pidexaudit failed: ${error?.message ?? error}`, "error"); } }, }); const startRunningPi = async (args: string | undefined, ctx: any) => { assertRuntimeBaseline(); const task = args?.trim(); const homeStatus = canonicalHomeStatus(); if (!homeStatus.ok) { ctx.ui.notify(homeStatus.message?.includes("not a valid") ? `${homeStatus.message}\nMove or repair that directory before starting PIDEX.` : canonicalHomeMissingMessage(), "warning"); return; } const selectedProjectRoot = await choosePidexProjectRoot(ctx); const deferProjectSelection = selectedProjectRoot === PIDEX_DEFER_PROJECT_SELECTION; let projectPipelineMode: ProjectPipelineModeResult | undefined; if (!selectedProjectRoot) { ctx.ui.notify("PIDEX project selection cancelled; no fallback was used.", "warning"); return; } if (!deferProjectSelection) { projectPipelineMode = await resolveProjectPipelineModeForCommand(ctx, selectedProjectRoot); if (projectPipelineMode?.mode) assertRuntimeBaseline(projectPipelineMode.mode); if (!projectPipelineMode) { ctx.ui.notify("PIDEX project mode selection cancelled; no fallback was used.", "warning"); return; } if (!projectPipelineMode.ok && projectPipelineMode.decision_required) { const reason = projectPipelineMode.reason ? `${projectPipelineMode.reason}. ${projectPipelineModeMissingGuidance()}` : projectPipelineModeMissingGuidance(); ctx.ui.notify(`PIDEX project mode is required before starting: ${reason}`, "warning"); return; } if (shouldStartProjectPipelineRunFlow(projectPipelineMode) && task) { await startProjectPipelineRunFlow({ ...ctx, cwd: selectedProjectRoot }, task); return; } } const projectPipelineTaskInterview = !deferProjectSelection && shouldStartProjectPipelineRunFlow(projectPipelineMode!) && !task; const authPreflight: DelegateAuthPreflightResult = deferProjectSelection || projectPipelineTaskInterview ? { ok: true, output: "" } : await runDelegateAuthPreflight(); const gitHookStatus = getGlobalGitHookStatus(); const sandboxState = resolveSandboxStateForProjectMode(deferProjectSelection ? undefined : projectPipelineMode); const sandboxProbe = sandboxState.enabled ? probeSandboxAvailability() : { ok: true, summary: sandboxEvidenceLine(sandboxState) }; if (sandboxState.enabled && !sandboxProbe.ok) { ctx.ui.notify(`PIDEX sandbox is enabled but unavailable: ${sandboxProbe.summary}`, "error"); return; } if (!authPreflight.ok) { ctx.ui.notify(authPreflight.failureKind === "launch" ? "pidex delegate auth checker could not be launched; authentication was not checked" : "pidex delegate authentication check ran and failed; see injected instructions", "error"); } else if (authPreflight.output) { ctx.ui.notify("pidex delegate auth preflight OK", "info"); } if (!deferProjectSelection) recordPreflightSkeleton(selectedProjectRoot, task, authPreflight.ok, gitHookStatus); const kickoff = [ "You are the pidex orchestrator.", `First read the orchestration skill at ${SKILL_PATH}.`, "Use the saved per-project PIDEX mode reported below. Do not use background/Telegram mode unless the user explicitly asks and accepts that it is scaffold-only.", "Use the pidex_agent tool for specialist handoffs, including pidex-wiki-hygienist for wiki hygiene/project memory maintenance. Keep project artifacts under agents.output/ and wiki/ using pidex-* conventions. Treat the final ROUTING block as authoritative and require context_file to exist. ROUTING route_to may be an pidex-* agent, user, or orchestrator for deterministic internal work such as browser-evidence collection.", "Run the pre-flight interview before invoking pidex-planner. If the fixed interview is insufficient, read ~/.pi/agent/skills/grill-me/SKILL.md and use it to ask one question at a time, with your recommended answer, until the epic is crisp.", deferProjectSelection ? "No project root was preselected by the extension because the user chose new/different path or the current directory did not look like a project. Run the skill Step 0/Step 1 project interview now, including the New project flow when the user chooses new." : "", `PIDEX global Git security hook: ${gitHookStatus}.`, deferProjectSelection ? "PIDEX project root: not selected yet; ask which project/name/path/new before any specialist handoff." : `Selected project root: ${selectedProjectRoot}`, deferProjectSelection ? recentPidexProjectsPrompt() : "", deferProjectSelection ? "PIDEX pipeline mode: not resolved yet because no project root is selected. After the project path exists, use the normal per-project mode rules for later runs; do not select a mode for the user's home directory." : `PIDEX pipeline mode: ${projectPipelineModeEvidenceLine(projectPipelineMode)}.`, deferProjectSelection ? "PIDEX mode instruction: project selection/new-project interview comes before mode selection on fresh starts." : projectPipelineModeInstructionLine(projectPipelineMode), `PIDEX sandbox preflight: ${sandboxState.enabled ? "hardened-pipeline enabled" : "off"}; ${sandboxProbe.summary}.`, sandboxState.enabled ? "Sandbox is internal hardening, not a user workflow change. Continue normal /pidex orchestration and dynamic routing. For source-mutating/risky phases, use the sandbox runtime helpers and include sandbox evidence or SANDBOX-SKIP in artifacts. Do not ask the user mid-run for sandbox configuration." : "Sandbox is off by config; do not probe Docker or alter normal /pidex routing for sandbox unless local config enables it.", deferProjectSelection ? "Delegate auth preflight is deferred until after project selection; do not start delegated agents before resolving project/mode and auth readiness." : projectPipelineTaskInterview ? "Delegate auth preflight is deferred during this Project Pipeline task interview. Confirm the task, then start the fail-closed in-container orchestrator; do not run host delegates." : authPreflight.ok ? "Delegate auth preflight passed for configured non-Pi providers." : authPreflight.failureKind === "launch" ? `Delegate auth checker launch/setup failed; authentication was not checked. Do not start delegated agents or silently fall back to another provider. Fix the launcher, or explicitly override selected agents to provider=pi. Output:\n${authPreflight.output}` : `Delegate authentication check ran and credentials/tooling failed. Do not start delegated agents or silently fall back to another provider. Resolve authentication, or explicitly override selected agents to provider=pi. Output:\n${authPreflight.output}`, task ? `Initial user task: ${task}` : "Initial user task: not provided; begin by asking which project and what deliverable.", ].join("\n\n"); ctx.ui.notify(deferProjectSelection ? "Starting PIDEX new-project interview" : `Starting pidex orchestrator (${projectPipelineMode?.mode ?? "host-direct"})`, "info"); pi.sendUserMessage(kickoff); }; pi.registerCommand("pidex-init-home", { description: "Initialize the canonical PIDEX runtime checkout at ~/pidex from the bootstrap package.", handler: async (_argLine, ctx) => initializePidexHome(ctx), }); pi.registerCommand("pidex", { description: "Start the pidex pidex-* software-delivery pipeline using the saved per-project mode.", handler: startRunningPi, }); pi.registerCommand("pd", { description: "Start the pidex pidex-* software-delivery pipeline using the saved per-project mode.", handler: startRunningPi, }); pi.registerCommand("pdproject", { description: "Manage local Project Pipeline Docker sandboxes (status/open/upgrade-pi/remove/preview).", handler: async (argLine, ctx) => { const homeStatus = canonicalHomeStatus(); if (!homeStatus.ok) { await ctx.ui.notify(homeStatus.message?.includes("not a valid") ? `${homeStatus.message}\nMove or repair that directory before running pdproject.` : canonicalHomeMissingMessage(), "warning"); return; } try { const parsed = parsePdProjectArgs(argLine); if (parsed.command === "upgrade-pi") await ctx.ui.notify("Checking sandbox Pi upgrade. Installation may take several minutes; keep this session open. Pipeline will not resume automatically.", "info"); const result = runPdProjectCommand(parsed, { projectRoot: ctx.cwd ?? process.cwd() }); await ctx.ui.notify(result.summary, result.ok ? "info" : "error"); } catch (error: any) { await ctx.ui.notify(`${error?.message ?? error}\n\n${pdProjectUsage()}`, "warning"); } }, }); pi.registerCommand("pdq", { description: "Run read-only PIDEX quality/self-improvement report.", handler: async (argLine, ctx) => { const homeStatus = canonicalHomeStatus(); if (!homeStatus.ok) { await ctx.ui.notify(homeStatus.message?.includes("not a valid") ? `${homeStatus.message}\nMove or repair that directory before running PDQ.` : canonicalHomeMissingMessage(), "warning"); return; } const result = runPidexQualityReport(ctx.cwd ?? PACKAGE_ROOT, argLine); await ctx.ui.notify(result.summary, result.ok ? "info" : "error"); }, }); pi.registerCommand("pdmem", { description: "Save a simple PIDEX project session memory snapshot to /wiki/session-memory/.", handler: async (argLine, ctx) => { try { const filePath = savePidexMemory(ctx, argLine); await ctx.ui.notify(`PIDEX memory saved: ${filePath}`, "info"); } catch (error: any) { await ctx.ui.notify(`pdmem failed: ${error?.message ?? error}`, "error"); } }, }); pi.registerCommand("pdwiki", { description: "Run a read-only PIDEX wiki hygiene audit for the current or given project root.", handler: async (argLine, ctx) => { const homeStatus = canonicalHomeStatus(); if (!homeStatus.ok) { await ctx.ui.notify(homeStatus.message?.includes("not a valid") ? `${homeStatus.message}\nMove or repair that directory before running wiki hygiene.` : canonicalHomeMissingMessage(), "warning"); return; } const raw = argLine?.trim(); const projectRoot = raw ? path.resolve(ctx.cwd ?? process.cwd(), raw) : path.resolve(ctx.cwd ?? process.cwd()); const result = runWikiHygieneAudit(projectRoot); await ctx.ui.notify(result.summary, result.ok ? "info" : "error"); }, }); pi.registerCommand("pdparallel", { description: "Inspect or test optional PIDEX parallel agent lanes.", handler: async (argLine, ctx) => { const homeStatus = canonicalHomeStatus(); if (!homeStatus.ok) { await ctx.ui.notify(homeStatus.message?.includes("not a valid") ? `${homeStatus.message}\nMove or repair that directory before inspecting parallel agents.` : canonicalHomeMissingMessage(), "warning"); return; } const result = runParallelAgentsCommand(argLine, ctx.cwd); await ctx.ui.notify(result.summary, result.ok ? "info" : "warning"); }, }); const pidexProjectTool: any = { name: "pidex_project", label: "PIDEX Project Pipeline", description: "Inspect Project Pipeline / Project Sandbox state through PIDEX Node helpers without requiring Bash. Read-only commands wrap the same redacted logic as /pdproject.", promptSnippet: "Inspect Project Pipeline registry, Docker health, runs, artifacts, credentials status, preview status/logs, and no-Bash diagnostics.", promptGuidelines: [ "Use pidex_project when troubleshooting Project Pipeline / Project Sandbox state, especially on native Windows where the bash tool may fail to start /bin/bash.", "Prefer pidex_project status/diagnose before spawning pidex-devops just to inspect Docker, registry, archive, dashboard DB, runs, or browser-smoke status.", "This tool is read-only. It does not run arbitrary shell or arbitrary Docker commands, does not sync source back to host, and redacts raw helper output/secrets.", "If multiple Project Pipeline projects are listed by status, ask the user which project id to inspect or choose only when the request clearly identifies one.", "Use pidex_agent only after pidex_project diagnostics show a real specialist task is needed.", ], parameters: PidexProjectParams as any, async execute(_toolCallId, params: any, _signal, _onUpdate, ctx) { try { const homeStatus = canonicalHomeStatus(); if (!homeStatus.ok) throw new Error(homeStatus.message?.includes("not a valid") ? `${homeStatus.message}\nMove or repair that directory before using pidex_project.` : canonicalHomeMissingMessage()); const parsed = buildPdProjectCommandFromToolParams(params); const result = runPdProjectCommand(parsed, { projectRoot: ctx.cwd ?? process.cwd() }); return { content: [{ type: "text", text: result.summary }], details: { ok: result.ok, no_fallback: result.no_fallback === true, command: params?.command || "", projectId: params?.projectId || null }, }; } catch (error: any) { throw error instanceof Error ? error : new Error(String(error)); } }, }; pi.registerTool(pidexProjectTool); pi.registerTool({ name: "pidex_project_maintenance", label: "PIDEX Project Maintenance", description: "Request a Pi-only upgrade in an existing Project Sandbox through native Node/Docker, without Bash or WSL. Requires an interactive user approval dialog for this invocation. No arbitrary Docker commands or automatic pipeline continuation.", promptSnippet: "When the user requests a sandbox Pi update, request the guarded native upgrade with an interactive confirmation.", promptGuidelines: [ "Inspect the target with pidex_project first. If multiple projects could match, ask the user which project id to maintain.", "Only action=upgrade-pi is supported. Never supply approval/confirmation fields; the actual user must approve the UI dialog.", "Do not retry automatically after cancellation, headless denial, busy state or HOLD. Do not reset leases, receipts, failed runs or budgets.", "Once approved, maintenance may take several minutes. Do not claim cancellation rolled back an update that already started; inspect its recorded status.", "This updates only Pi in the existing container. It does not update Node/the image or authorize resuming the held pipeline.", ], parameters: PidexProjectMaintenanceParams as any, async execute(_toolCallId, params: any, signal, _onUpdate, ctx) { const homeStatus = canonicalHomeStatus(); if (!homeStatus.ok) throw new Error("Pi maintenance requires a valid canonical PIDEX checkout; no changes made."); const projectId = params?.projectId; const result = await requestProjectPiMaintenance(params, ctx, { signal }); return { content: [{ type: "text", text: result.summary }], details: { ok: result.ok, no_fallback: true, action: "upgrade-pi", projectId }, }; }, } as any); const rpAgentTool: any = { name: "pidex_agent", label: "pidex Agent", description: "Run a bundled pidex-* specialist agent through config/agents.json. Defaults to lean Pi subprocesses, with optional Codex CLI delegates for configured agents. Raw child logs are stored outside the parent Pi session.", promptSnippet: "Run a bundled pidex-* specialist agent using pidex provider routing from config/agents.json.", promptGuidelines: [ "Use pidex_agent for pidex specialist handoffs such as pidex-planner, pidex-critic, pidex-implementer, pidex-code-reviewer, pidex-qa, pidex-uat, pidex-devops, pidex-wiki-hygienist, pidex-retrospective, and pidex-pi.", "Host-direct and hardened-pipeline primary calls use config/agents.json only. Never pass provider, model, or effort.", "Host secondary calls require exact laneId plus trigger. PIDEX resolves runner provider, model, and effort from currently eligible parallel-lane status; never construct route fields.", "Configured optional parallel agents resolve from PIDEX_PARALLEL_AGENTS_CONFIG, then /config/parallel-agents.local.json, then the disabled public /config/parallel-agents.json default. Project Pipeline orchestration owns its separate in-container path; direct project-pipeline pidex_agent calls require projectId plus exact expected input/output paths and never fall back to the host.", "When using pidex_agent, pass complete context in the task, including project cwd, current epic, relevant agents.output paths, expected output file, and required ROUTING behavior. The final ROUTING block must include context_file, not doc. route_to may be an pidex-* agent, user, or orchestrator for deterministic internal follow-up.", "For JS/TS security or QA handoffs, remind pidex-security/pidex-qa to run the relevant Fallow gate or document FALLOW-SKIP.", "Specialists should write full artifacts to files and keep final responses short; pidex_agent will truncate oversized final text and store raw child logs under pidex/state/runs/.", ], parameters: PidexAgentParams as any, async execute(_toolCallId, params: any, signal, onUpdate, ctx) { try { params = normalizePublicReviewIdentity(params); const homeStatus = canonicalHomeStatus(); if (!homeStatus.ok) throw new Error(homeStatus.message?.includes("not a valid") ? `${homeStatus.message}\nMove or repair that directory before using pidex_agent.` : canonicalHomeMissingMessage()); const agentCwd = path.resolve(params.cwd ?? ctx.cwd); const agentProjectMode = runProjectPipelineModeResolver(agentCwd); if (params.projectId || (agentProjectMode.ok && agentProjectMode.mode === "project-pipeline")) { const projectResult = runProjectPipelineAgentTool(params); const routingText = projectResult.routing ? `\n\n` : ""; return { content: [{ type: "text", text: `Project Pipeline ${params.agent} complete in /workspace. context_file=${projectResult.context_file}${projectResult.reviewCompletion?.status ? `; review_completion=${projectResult.reviewCompletion.status}` : ""}${projectResult.routing_recovered ? "; routing recovered from exact artifact" : ""}${projectResult.project_mirror?.degraded ? `; archive complete; project mirror degraded (${projectResult.project_mirror.status})` : projectResult.project_mirror?.status ? `; project mirror ${projectResult.project_mirror.status}` : ""}${routingText}` }], details: { ok: true, projectId: params.projectId, project_run_id: projectResult.project_run_id, context_file: projectResult.context_file, archive_context_file: projectResult.archive_context_file, project_mirror: safeProjectMirrorSummary(projectResult.project_mirror), sync_degraded: projectResult.project_mirror?.degraded === true, routing_recovered: projectResult.routing_recovered === true, write_fence: projectResult.write_fence?.status || null, reviewCompletion: safeReviewCompletionSummary(projectResult.reviewCompletion), no_fallback: true }, }; } const result = await executeHostAgentBoundary(params, { agentCwd, agentProjectMode, signal, onUpdate: (text: string) => onUpdate?.({ content: [{ type: "text", text: clipEnd(text, MAX_UPDATE_CHARS) }], details: {} }), }); const closeoutStatus = (result as any).closeoutCompletion; const contentText = formatToolContent(result) + (closeoutStatus ? `\n\nPIDEX closeout: ${closeoutStatus.status}; ${closeoutStatus.learningDisposition ? `learning=${closeoutStatus.learningDisposition}; replayed=${closeoutStatus.replayed === true}; ` : ""}pending obligations=${JSON.stringify(closeoutStatus.pendingObligations)}. Each successful dispatch was validated against ITS OWN artifact/nonce. Different calls MUST have different dispatch IDs; never compare IDs across agents as an equality requirement. This is not pipeline completion; use the canonical terminal validator after task obligations are done.` : ""); const missingRouting = !hasRoutingBlock(result.finalText); // A successful producer closeout already validated exact ROUTING, // artifact and invocation (v2 finish or v3 receipt/replay). Do not // contradict that result with the ordinary line-only/disk check. const closeoutValidated = closeoutStatus?.status === "dispatch_completed"; const invalidContextFile = !closeoutValidated && !missingRouting && !hasValidRoutingContextFile(result.finalText, params.cwd ?? ctx.cwd); if (result.exitCode !== 0 || missingRouting || invalidContextFile) { const reason = result.exitCode !== 0 ? `pidex_agent '${params.agent}' failed with exitCode=${result.exitCode}` : missingRouting ? `pidex_agent '${params.agent}' finished without ROUTING block` : `pidex_agent '${params.agent}' finished with invalid ROUTING context_file`; throw new Error(`${reason}\n\n${contentText}`); } return { content: [{ type: "text", text: contentText }], details: safeDetailsForResult(result), }; } catch (error: any) { throw error instanceof Error ? error : new Error(String(error)); } }, }; pi.registerTool(rpAgentTool); }