import type { Command } from 'commander'; import http from 'http'; import https from 'https'; import type { Config } from '../types/index.js'; import type { LlmProviderType } from '../types/config.schema.js'; import type { RemotePlanningBundle } from '../utils/planning-credentials.js'; import { type SandboxProfile } from '../utils/sandbox-profile.js'; /** * Job status type */ type JobStatus = 'queued' | 'running' | 'completed' | 'failed' | 'cancelled'; /** * Launch command - Trigger a launch job on hula-project server * * Usage: hula launch [options] * Usage: hula launch --show * Usage: hula launch --logs * Usage: hula launch --logs --full (whole stored run log, not the tail) */ /** * Commander accumulator for the repeatable `--plan =` option. * Each occurrence is parsed as `repoName=relativePath` and merged into the * override map. A missing `=` throws a clear error. */ export declare function collectPlanOverrides(value: string, previous: Record): Record; export declare function launchCommand(program: Command, config: Config): void; interface LaunchOptions { url?: string; apiKey?: string; duration?: number; launch?: string; worktree?: string | boolean; resume?: number; fix?: string; regression?: boolean; /** --contribution: force guest contribution mode (overrides config). */ contribution?: boolean; /** --link-guidelines: cite the Responsible Contribution Guidelines in the * guest PR disclosure (overrides config `linkGuidelines`). */ linkGuidelines?: boolean; /** --skip-wait: skip the `waitForMaintainerDays` waiting window. */ skipWait?: boolean; skipRegression?: boolean; kill?: boolean; killAndRelaunch?: boolean; verbose?: boolean; test?: boolean; handoff?: string; /** --issue: existing GitHub issue reference (number, #number, or URL). */ issue?: string; /** --pr: existing pull request reference (number, #number, or URL). */ pr?: string; /** --base: branch to cut the work branch from and open the PR into. */ base?: string; show?: string; logs?: string; lines?: number; type?: 'log' | 'output'; /** --full: with --logs, fetch the whole persisted Task.logContent (include=logs). */ full?: boolean; editor?: boolean; /** --provider: LLM provider type (claude | openai | openrouter). */ provider?: string; /** --provider-key: the provider credential (ephemeral). */ providerKey?: string; containerCpu?: number; containerMemory?: number; containerDisk?: number; /** --groundUntil: Grounding Loop target confidence (0-100); defaults to the merge threshold. */ groundUntil?: number; /** --maxGroundingRounds: Grounding Loop hard round cap (≥1); enables the opt-in loop. */ maxGroundingRounds?: number; /** --sandbox-profile: base | android[:17|21]; overrides config.sandboxProfile. */ sandboxProfile?: string; updateNotificationUrl?: string; updateNotificationNameTag?: string; /** Parent folder containing sibling repos to fan out over. */ folder?: string; /** Explicit group id (join/retry an existing group). */ groupId?: string; /** Repeatable `--plan =` overrides, keyed by repo directory name. */ plan?: Record; /** * `--show-group [id]`: string when an id was given, `true` for the bare flag * (resolve the latest local group record), undefined when not passed. */ showGroup?: string | boolean; /** * `--yes`: skip the group-launch confirmation prompt and allow per-repo plan * auto-resolve without a TTY. The /hula-launch skill path runs the CLI as a * piped subprocess (never a TTY), so the skill passes this to opt in to the * same auto-resolve + roster flow an interactive terminal gets. */ yes?: boolean; } /** * Validate the `--kill` / `--kill-and-relaunch` flag combination. * * - The two flags are mutually exclusive. * - `--kill` composes with no other launch-only flag (`--resume`, `--fix`, * `--test`, `--handoff`, `--regression`). * - `--kill-and-relaunch` composes normally with every launch flag. * * Returns an error message when the combination is invalid, or `null` when OK. */ export declare function validateKillFlags(options: LaunchOptions): string | null; /** * Whether a plan path is required for the given flag combination. * `--kill` needs only the tracking name; every other mode needs a plan. */ export declare function requiresPlanPath(options: LaunchOptions): boolean; /** * Validate the `--issue` / `--pr` / `--base` flag matrix. * * - A plan file may accompany `--issue` OR `--pr`, but not both — with both and * a plan file there is no single instruction source. Without a plan file, * `--issue` and `--pr` combine (plan generated from both). * - `--base` is meaningless with `--pr` (the base comes from the PR itself). * - `--issue` / `--pr` are single-repo launch flows, incompatible with the * read/group/kill flags in {@link SOURCE_INCOMPATIBLE_FLAGS}. * * Returns an error message when the combination is invalid, or `null` when OK. */ export declare function validateSourceFlags(options: LaunchOptions, hasPlanPathPositional: boolean): string | null; /** * Validate the multi-repo `--folder` / `--group-id` / `--plan` flag matrix. * * - `--folder` resolves plans per repository, so it is incompatible with a * positional `` and with the read/resume flags `--show`, `--logs`, * `--resume`, `--fix`. * - `--plan` (per-repo override) only makes sense with `--folder`. * - `--group-id` alone (no `--folder`) is allowed — it attaches a single-repo * launch to an existing group. * - `--kill --folder` is allowed (cancel-only fan-out). * * Returns an error message when the combination is invalid, or `null` when OK. */ export declare function validateFolderFlags(options: LaunchOptions, hasPlanPathPositional: boolean): string | null; /** * Derive the request `conflictMode` from the CLI flags (plus an optional explicit * override used when re-invoking after an interactive 409 resolution). * * Returns `undefined` for the default `reject` path so the request body stays * byte-identical to today's — the key is only added for `killAndRelaunch`. */ export declare function deriveConflictMode(options: LaunchOptions, override?: 'killAndRelaunch'): 'killAndRelaunch' | undefined; /** * Build the request body for a cancel-only (`--kill`) call. Carries NO secrets — * only the tracking name, repository id, and the `kill` conflict mode. */ export declare function buildKillRequestBody(issueName: string, repositoryId: string): Record; /** * Assemble the outgoing `steps` request field from the config-file `steps` * object plus the `--skip-regression` launch flag, and enforce the two * cross-field conflict rules the server also enforces (mirrors `resolveSteps()` * in hula-server's ralph-run route, PR #442). Doing the checks client-side turns * a would-be HTTP 400 round trip into an immediate, identically-worded failure: * 1. `bugfix.model` and `mergeConflict.model` map to the same * `RALPH_BUGFIX_MODEL` env var, so they cannot be set to different values. * 2. `regression.skip: true` (whether from config or `--skip-regression`) * contradicts the legacy `--regression` force-on flag. * * On success returns `{ steps }` where `steps` is `undefined` when there is * nothing to send (no config `steps`, no `--skip-regression`) — keeping the * request body byte-identical to pre-change behavior. On failure returns * `{ error }` with the server's exact error text. */ export declare function resolveSteps(configSteps: Config['steps'], skipRegressionFlag: boolean | undefined, legacyRegressionFlag: boolean | undefined): { steps?: Record; error?: string; }; /** * Connection options for POSTs to the launch endpoint: a fresh connection per * request instead of Node's pooled keep-alive socket. * * Node 19+ keeps idle sockets for 5s and reuses them. The launch flow makes a * request to the same host (the subscription check) a few seconds before the * POST, so the POST could land on a socket the server side was closing at * almost the same moment. The request is then never seen by the server and * the CLI waits out its whole timeout (a production launch "timed out" with no * trace in the server logs). Reproduced against hublaunch.site: GET, wait * 4.8-4.98s, POST hung in 3 of ~100 tries on the keep-alive agent and 0 of 40 * with a fresh connection. One extra TLS handshake per launch is negligible. */ export declare const FRESH_CONNECTION: { readonly httpAgent: http.Agent; readonly httpsAgent: https.Agent; }; /** Client-side ceiling for the launch POST (ms). */ export declare const LAUNCH_REQUEST_TIMEOUT_MS = 120000; /** * Explain why a launch was refused for billing reasons. * * `subscriptionStatus` is what the server reported for this account (`null` * when it has never had a subscription, so a free trial is still available); * `serverError` is the server's own 402 message when the refusal came from the * launch endpoint rather than the pre-flight check — it is more specific than * anything the CLI can infer ("Out of PR credits — buy more to launch"), so it * wins. `actionUrl` is the `upgradeUrl`/`buyCreditsUrl` from that same payload. * * `refusal` says which 402 it was: `'credits'` (the server sent `buyCreditsUrl` * — the account has Pro but no credits left) or `'plan'` (it sent `upgradeUrl`). * * With none of them supplied, the wording stays generic rather than asserting a * tier the CLI has not confirmed: an unknown status (`undefined`) is NOT treated * as "never subscribed", so nobody is offered a free trial they may not get. */ export interface BillingRefusal { serverUrl: string; subscriptionStatus?: string | null; serverError?: string; actionUrl?: string; refusal?: 'credits' | 'plan'; } /** The "Your options" lines printed under a billing refusal. */ export declare function billingRefusalOptions(opts: BillingRefusal): string[]; /** * Result of a single-repo launch attempt. In folder (group) mode this is * captured per repo so the fan-out can continue past failures and print a * summary; in single-repo mode a failure exits the process directly. */ export interface RepoLaunchResult { ok: boolean; planId?: string; status?: number; message?: string; } /** * Inputs to {@link buildRalphRunRequestBody}. Every optional field is omitted * from the body when absent, keeping non-group / default request bodies * byte-identical to the pre-fan-out CLI. */ export interface RalphRunBodyInput { issueName: string; planPath: string; repositoryId: string; duration?: number; /** From `--launch`; serialized as `ralphPath` for server compatibility. */ launchPath?: string; worktree?: string | boolean; resume?: number; fix?: string; regression?: boolean; steps?: Record; /** * Merge-confidence policy from `hublaunch.config.js`. Forwarded verbatim as * `mergePolicy` (hula-server merge-confidence plan). Omitted when absent so * legacy bodies stay byte-identical. */ mergePolicy?: { minConfidence?: number; autoMerge?: boolean; }; /** * Opt-in Grounding Loop config, from the `--groundUntil` / * `--maxGroundingRounds` flags or `hublaunch.config.js`'s `groundingLoop` * block. Forwarded verbatim as `groundingLoop`. Omitted entirely when * grounding is OFF (the default) so legacy bodies stay byte-identical. * `groundUntil` is optional (the server defaults it to the merge threshold). */ groundingLoop?: { groundUntil?: number; maxGroundingRounds: number; }; /** * "Defaults for everyone" PR-hygiene block from `hublaunch.config.js`. * Forwarded verbatim as `defaults`. Omitted when every behaviour is on (the * common case) so legacy bodies stay byte-identical. */ defaults?: { styleSurvey?: boolean; scopeLock?: boolean; contributingDocs?: boolean; environmentFailureOnce?: boolean; windowsSafeNames?: boolean; }; /** * Guest contribution mode. Forwarded unresolved (`true | 'auto'`); the server * resolves `'auto'` against fork metadata. Omitted when the inert default * `false` so legacy bodies stay byte-identical. */ contributionMode?: boolean | 'auto'; /** * Issue/plan split. Forwarded unresolved (`true | false`); the server * resolves `'auto'` against ownership + contribution mode. Omitted when the * inert default `'auto'` so legacy bodies stay byte-identical. */ issuePlanSplit?: boolean | 'auto'; /** Disclosure opt-in. Forwarded only when `true`; omitted otherwise. */ linkGuidelines?: boolean; /** * Project-declared end-to-end stage directives from `hublaunch.config.js`'s * `e2e` block. Forwarded as `e2e` (worker maps each to a `RALPH_E2E_*` env * var). The config's `e2e.envVars` NAMES are resolved to values and folded * into `envVars` instead, so they are NOT carried here. Omitted when the * project declares no directives so legacy bodies stay byte-identical. */ e2e?: { tools?: string[]; setup?: string[]; ready?: string; timeoutMinutes?: number; readyTimeoutMinutes?: number; postReport?: boolean; }; test?: boolean; conflictMode?: 'killAndRelaunch'; /** Maps to RalphRunRequest.groupId (hula-server PR #505). */ groupId?: string; verbose?: boolean; handoff?: string; clientSessionId?: string; /** * LLM provider selection + credential. Replaces the pre-multi-provider * `anthropicApiKey` field (hula-server multi-provider plan). `authToken` is the * ephemeral credential; `type` selects the server-side agent harness. */ provider: { type: LlmProviderType; authToken: string; }; containerResources?: { cpu: number; memory: number; disk: number; }; /** Non-base sandbox profile; omitted for base (the server default). */ sandboxProfile?: SandboxProfile; updateNotificationUrl?: string; updateNotificationNameTag?: string; githubToken?: string; envVars?: Record; /** * True when the feature branch (with the plan committed) already exists on * origin — the plan-branch flow. The server then checks out the existing * branch in the sandbox instead of creating one from the base branch. */ branchExists?: boolean; /** * Launch-from-existing linkage (optional; omitted when absent so legacy * bodies stay byte-identical). An older server ignores unknown fields. */ /** Existing GitHub issue to link (`Closes #N`). */ issueNumber?: number; /** Existing pull request being continued. */ prNumber?: number; /** The PR's head branch (the work branch; may contain `/`, unsanitized). */ headBranch?: string; /** The PR's / --base target branch the PR opens into. */ baseBranch?: string; } /** * Parse a beforeLaunch hook's stdout into an envVars object to merge into the * launch request. The contract is `{"envVars": {"KEY": "value", ...}}`. * * Returns `undefined` when the hook has nothing to contribute: * - empty stdout * - `{}` / valid JSON with no `envVars` key * - `{"envVars": {}}` (empty object) * * Throws when the contract is violated (Edge Case 3 — abort rather than launch * against the wrong resources): * - stdout is not valid JSON * - `envVars` is present but not a plain object */ export declare function parseBeforeLaunchHookOutput(stdout: string): Record | undefined; /** Already-resolved inputs for {@link buildLaunchSyncBundle}. */ export interface LaunchSyncBundleInput { providerType: string; authToken: string; githubToken?: string; envVars?: Record; updateNotificationUrl?: string; updateNotificationNameTag?: string; } /** * Build the credential bundle `hula launch` hands to `syncCredentialSet` after a * successful launch. Mirrors byte-for-byte what the launch transmitted: the * provider credential, GitHub token, forwarded env vars, and the effective * update-notification URL / name tag (CLI flag → config → env, flags included). * Each optional field is set only when truthy so the stored set matches the * launch request exactly. */ export declare function buildLaunchSyncBundle(input: LaunchSyncBundleInput): RemotePlanningBundle; /** * Capture the env vars to persist in the credential set BEFORE the * beforeLaunch hook runs. A shallow copy so the later * `envVars = { ...envVars, ...hookEnvVars }` merge cannot mutate it; hook * output is per-launch and must never reach the stored set. Returns * `undefined` when nothing was collected so callers omit the field. */ export declare function snapshotEnvVarsForSync(envVars: Record | undefined): Record | undefined; /** * Assemble the `POST /api/v1/ralph-run` request body from already-resolved * inputs. Fields are inserted in a fixed order and only when present, so a * launch with no group flag produces a body byte-identical to the pre-fan-out * CLI (verified by unit test — AC5). */ /** * Project the config's `e2e` block to the directives forwarded on the request * body, dropping `envVars` (its NAMES are resolved to values and folded into * the request's `envVars` instead — see the .env collection in `runLaunch`). * Returns `undefined` when the result carries no directives so the caller omits * the field (byte-identical legacy bodies). Pure. */ export declare function forwardE2eDirectives(e2e: Config['e2e']): RalphRunBodyInput['e2e'] | undefined; export declare function buildRalphRunRequestBody(input: RalphRunBodyInput): Record; /** * Resolve the opt-in Grounding Loop config from CLI flags and project config. * * Precedence: flag > project config > default (OFF). Grounding is enabled only * when `maxGroundingRounds` resolves to a value (the required hard cap); * `groundUntil` is optional and, when omitted, is defaulted server-side to the * resolved merge-confidence threshold. Returns a discriminated result: * `{ groundingLoop }` when enabled (and valid), `{}` when OFF, or `{ error }` * with a precise range message. Pure — never throws, never exits. */ export declare function resolveGroundingLoop(opts: { groundUntil?: number; maxGroundingRounds?: number; }, config: { groundingLoop?: { groundUntil?: number; maxGroundingRounds?: number; }; }): { groundingLoop?: { groundUntil?: number; maxGroundingRounds: number; }; error?: string; }; /** * Handle API errors consistently * (exported for reuse by sibling commands, e.g. `hula info`) */ export declare function handleApiError(error: unknown, serverUrl: string): void; /** * Merge-assessment summary as stored on the task (hula-server merge-confidence * plan). Only the fields `--show` renders are typed; the server sends more. */ export interface JobMergeAssessmentSummary { confidence?: number | null; verdict?: string | null; threshold?: number | null; } /** * Normalized `hula launch --show` payload. * * `GET /api/v1/ralph-run/:issueName/status` returns the task row directly * (`status`, `pid`, `startedAt`, `completedAt`, `exitCode`, `workerHost`, * `startedBy`, `planPath`, `heartbeatStale`, `mergeAssessment`). The CLI used * to read a `jobStatus` / `jobPid` / … shape (the one the harden-run status * route still uses), so `--show` printed "No job found" for every task. * `normalizeJobStatus` accepts both shapes. */ export interface JobStatusSummary { status: JobStatus | string; pid: number | null; startedAt: string | null; completedAt: string | null; exitCode: number | null; workerHost: string | null; startedBy: string | null; planPath: string | null; prSource: string | null; heartbeatStale: boolean; mergeAssessment: JobMergeAssessmentSummary | null; } /** * Map a status response (current task shape or legacy `job*` shape) to one * summary. Returns null when no status is present, which the caller reports * as "No job found". */ export declare function normalizeJobStatus(data: Record | null | undefined): JobStatusSummary | null; /** * Deprecated ralph command - alias for launch * @deprecated Use launchCommand instead */ export declare function ralphCommand(program: Command, config: Config): void; export {}; //# sourceMappingURL=launch.d.ts.map