/** * `shard` CLI entrypoint. * * Dispatches on `process.argv` subcommand: * `shard web` (or no subcommand) → daemon-backed browser client. * `shard agent` → headless stdio server speaking the per-agent protocol. * `shard config ` → print loaded config (redacted) / the XDG config path. * `shard models ` → `/v1/models` discovery + config overrides. * * Configuration (config is canonical; env stays as overrides + secret fallback, per saga * 9mre6k): provider/model resolved from `~/.config/shard/config.json` (XDG) via * `loadConfig()` + `resolveModel()`. `--model`/`--provider` flags + `SHARD_MODEL`/ * `OPENAI_MODEL`/`SHARD_PROVIDER` env override config (flag > env > config — CI / one-off * runs win). API key read from the resolved provider's `auth.envVar` (bearer) at runtime — * NEVER persisted or logged. `--api-key`/`--base-url`/`OPENAI_BASE_URL` stay as test-only * escape hatches that override the resolver's output. Session path from `--session` or a * generated one under `~/.shard/sessions/.jsonl`; cwd = `process.cwd()`. No hardcoded * secrets. */ import { homedir } from 'node:os'; import { join, resolve } from 'node:path'; import { mkdirSync, existsSync } from 'node:fs'; import { isIP } from 'node:net'; import { createAgentSessionFromCore, createDefaultCore, DEFAULT_CONTEXT_WINDOW, } from '@/core/default'; import { handleInlineApproval } from '@/cli/commands/inline-approval'; import type { DaemonOptions } from '@/daemon'; import type { ModelDescriptor } from '@/contracts/model'; import type { ShardConfig } from '@/cli/config/schema'; const SUBCOMMANDS = new Set(['agent', 'config', 'models', 'web', 'daemon', 'shardd', 'gc', 'setup', 'new', 'worktree']); /** Flags that consume the following argv element as their value. */ const VALUE_FLAGS = new Set([ '--model', '-m', '--provider', '--session', '-s', '--api-key', '--base-url', '--cwd', '--id', '--socket', '--home', '--managed-manifest', '--daemon-socket', '--web-host', '--web-port', '--public-host', '--allowed-origin', '--trust-proxy', ]); /** Print usage to stderr + exit non-zero. */ function usageExit(): never { process.stderr.write( 'Usage: shard [web|agent|daemon|config|models|setup|gc|new|worktree] [options]\n' + ' agent headless stdio server speaking the per-agent protocol (PROTOCOL v0.1)\n' + ' daemon long-lived multi-agent daemon `shardd` (unix-socket, PROTOCOL §6/§8/§10)\n' + ' config config subcommand: `config show` | `config path` | `config init`\n' + ' models model catalog: `models list [--provider p]` | `models show `\n' + ' new create a managed worktree or launch a fresh Agent in an existing workspace\n' + ' worktree list, inspect, and safely retire daemon-managed worktrees\n' + ' web start the daemon + browser client (prints URL); admin subcommands:\n' + ' approve , clients, revoke , token create|rotate\n' + ' setup interactive first-provider config wizard\n' + ' gc remove empty orphaned agent directories under ~/.shard/agents\n' + 'Options: --model/-m, --provider, --session/-s, --api-key, --base-url, --cwd, --id, --socket, --home, --trust-project\n' + 'Daemon: --web-host , --web-port , --public-host , --allowed-origin (repeatable), --trust-proxy , --allow-insecure-remote\n', ); process.exit(1); } /** Resolve the CLI options from argv + env. */ interface CliOptions { modelName?: string; providerKey?: string; sessionPath: string; /** Working directory for tools. Defaults to `process.cwd()`. */ cwd: string; /** Agent id (stdio server); generated if not supplied. */ agentId?: string; /** Test-only escape hatch: overrides the resolver's bearer token. */ apiKey?: string; /** Test-only escape hatch: overrides the resolver's base URL. */ baseURL?: string; /** Explicitly trust project-local instructions, rules, skills, agents, and extensions. */ trustProject: boolean; runtimeGeneration?: string; daemonLease?: string; /** Managed-manifest path (saga u4uo9m.1): a daemon-managed subagent loads pinned * system_prompt/launch_policy/depth/timeout/alias/definition config from here instead * of argv, and connects back to the daemon over `daemonSocket`. */ managedManifest?: string; /** Daemon unix-socket path for a managed child to connect back to (saga u4uo9m.1). */ daemonSocket?: string; } function parseOptions(rest: string[]): CliOptions { let modelName: string | undefined; let providerKey: string | undefined; let sessionPath: string | undefined; let apiKey: string | undefined; let baseURL: string | undefined; let cwd: string | undefined; let agentId: string | undefined; let trustProject = false; let runtimeGeneration: string | undefined; let daemonLease: string | undefined; let managedManifest: string | undefined; let daemonSocket: string | undefined; for (let i = 0; i < rest.length; i++) { const arg = rest[i]; if (arg === undefined) continue; if (arg === '--model' || arg === '-m') { modelName = rest[++i]; } else if (arg === '--provider') { providerKey = rest[++i]; } else if (arg === '--session' || arg === '-s') { sessionPath = rest[++i]; } else if (arg === '--api-key') { apiKey = rest[++i]; } else if (arg === '--base-url') { baseURL = rest[++i]; } else if (arg === '--cwd') { cwd = rest[++i]; } else if (arg === '--id') { agentId = rest[++i]; } else if (arg === '--trust-project') { trustProject = true; } else if (arg === '--runtime-generation') { runtimeGeneration = rest[++i]; } else if (arg === '--daemon-lease') { daemonLease = rest[++i]; } else if (arg === '--managed-manifest') { managedManifest = rest[++i]; } else if (arg === '--daemon-socket') { daemonSocket = rest[++i]; } else if (arg.startsWith('--model=')) { modelName = arg.slice('--model='.length); } else if (arg.startsWith('--provider=')) { providerKey = arg.slice('--provider='.length); } else if (arg.startsWith('--session=')) { sessionPath = arg.slice('--session='.length); } else if (arg.startsWith('--api-key=')) { apiKey = arg.slice('--api-key='.length); } else if (arg.startsWith('--base-url=')) { baseURL = arg.slice('--base-url='.length); } else if (arg.startsWith('--cwd=')) { cwd = arg.slice('--cwd='.length); } else if (arg.startsWith('--id=')) { agentId = arg.slice('--id='.length); } else { process.stderr.write(`shard: unknown argument: ${arg}\n`); usageExit(); } } const resolvedSession = sessionPath ?? defaultSessionPath(); // Ensure the parent dir exists (createDefaultCore's SessionStore.create also mkdir -p's, // but doing it here yields a cleaner error path before the SDK is built). mkdirSync(resolve(resolvedSession, '..'), { recursive: true }); return { ...(modelName !== undefined ? { modelName } : undefined), ...(providerKey !== undefined ? { providerKey } : undefined), sessionPath: resolvedSession, cwd: cwd ?? process.cwd(), trustProject, ...(agentId !== undefined ? { agentId } : undefined), ...(apiKey !== undefined ? { apiKey } : undefined), ...(baseURL !== undefined ? { baseURL } : undefined), ...(runtimeGeneration !== undefined ? { runtimeGeneration } : undefined), ...(daemonLease !== undefined ? { daemonLease } : undefined), ...(managedManifest !== undefined ? { managedManifest } : undefined), ...(daemonSocket !== undefined ? { daemonSocket } : undefined), }; } /** Default per-agent session path under `~/.shard/sessions/.jsonl`. */ function defaultSessionPath(): string { const sessionsDir = join(homedir(), '.shard', 'sessions'); if (!existsSync(sessionsDir)) { mkdirSync(sessionsDir, { recursive: true }); } const id = crypto.randomUUID(); return join(sessionsDir, `${id}.jsonl`); } /** Extract a `--provider ` (or `--provider=value`) from a rest array. */ function extractProviderFlag(rest: string[]): { providerKey?: string; leftover: string[] } { const leftover: string[] = []; let providerKey: string | undefined; for (let i = 0; i < rest.length; i++) { const a = rest[i]; if (a === undefined) continue; if (a === '--provider') { providerKey = rest[++i]; } else if (a.startsWith('--provider=')) { providerKey = a.slice('--provider='.length); } else { leftover.push(a); } } return { ...(providerKey !== undefined ? { providerKey } : undefined), leftover }; } /** Route `shard config ` sub-subcommands. Returns exit code (process.exit'd inside). */ async function runConfigSub(rest: string[]): Promise { const { runConfigShow, runConfigPath, runConfigInit } = await import('@/cli/commands/config-cmd'); const sub = rest[0]; if (sub === 'show') return runConfigShow(); if (sub === 'path') return runConfigPath(); if (sub === 'init') return runConfigInit(); process.stderr.write('Usage: shard config \n'); return 1; } /** Parsed daemon/web flags shared by `shard daemon` and `shard web` (no admin sub). */ interface DaemonFlagResult { socketPath?: string; home?: string; restartPolicy?: string; trustProject: boolean; /** Remote/web daemon options assembled from the --web-* / --public-host / origin flags. */ remoteOpts: DaemonOptions; help: boolean; /** First parse error (invalid flag value); the caller surfaces it. */ error?: { message: string; code: number }; } /** * Parse the daemon/web flag grammar shared by `shard daemon` and `shard web` (no admin * subcommand). Handles `--socket`, `--home`, `--restart`, `--trust-project`, and the * remote/web surface (`--web-host`, `--web-port`, `--public-host`, `--allowed-origin`, * `--trust-proxy`, `--allow-insecure-remote`). Unknown args are silently skipped * (matching the historical daemon parser). Returns the parsed result; `error`/`help` are * surfaced by the caller so each subcommand can print its own usage text. */ function parseDaemonFlags(rest: string[]): DaemonFlagResult { let socketPath: string | undefined; let home: string | undefined; let restartPolicy: string | undefined; let trustProject = false; let webHost: string | undefined; let webPort: number | undefined; let publicHost: string | undefined; const allowedOrigins: string[] = []; let trustedProxyUpstream: string | undefined; let allowInsecureRemote = false; let help = false; let error: { message: string; code: number } | undefined; for (let i = 0; i < rest.length; i++) { const a = rest[i]; if (a === undefined) continue; if (a === '--socket') { const value = rest[++i]; if (socketPath !== undefined || value === undefined || value.length === 0 || value.startsWith('-')) { error = { message: 'shard: invalid or duplicate --socket flag', code: 2 }; break; } socketPath = value; } else if (a.startsWith('--socket=')) { const value = a.slice('--socket='.length); if (socketPath !== undefined || value.length === 0) { error = { message: 'shard: invalid or duplicate --socket flag', code: 2 }; break; } socketPath = value; } else if (a === '--home') { home = rest[++i]; } else if (a.startsWith('--home=')) { home = a.slice('--home='.length); } else if (a === '--restart') { restartPolicy = rest[++i]; } else if (a === '--trust-project') { trustProject = true; } else if (a.startsWith('--restart=')) { restartPolicy = a.slice('--restart='.length); } else if (a === '--web-host') { webHost = rest[++i]; if (webHost === undefined || webHost.length === 0) { error = { message: 'shard: --web-host requires a value', code: 2 }; break; } } else if (a.startsWith('--web-host=')) { webHost = a.slice('--web-host='.length); } else if (a === '--web-port') { const value = rest[++i]; const parsed = value === undefined ? NaN : Number(value); if (!Number.isInteger(parsed) || parsed < 0 || parsed > 65535) { error = { message: `shard: invalid --web-port: ${value ?? ''}`, code: 2 }; break; } webPort = parsed; } else if (a.startsWith('--web-port=')) { const parsed = Number(a.slice('--web-port='.length)); if (!Number.isInteger(parsed) || parsed < 0 || parsed > 65535) { error = { message: `shard: invalid --web-port: ${a.slice('--web-port='.length)}`, code: 2 }; break; } webPort = parsed; } else if (a === '--public-host') { publicHost = rest[++i]; if (publicHost === undefined || publicHost.length === 0) { error = { message: 'shard: --public-host requires a value', code: 2 }; break; } } else if (a.startsWith('--public-host=')) { publicHost = a.slice('--public-host='.length); } else if (a === '--allowed-origin') { const value = rest[++i]; if (value === undefined || value.length === 0) { error = { message: 'shard: --allowed-origin requires a value', code: 2 }; break; } allowedOrigins.push(value); } else if (a.startsWith('--allowed-origin=')) { allowedOrigins.push(a.slice('--allowed-origin='.length)); } else if (a === '--trust-proxy') { const value = rest[++i]; // `loopback` or an IPv4/IPv6 literal; reject anything else loudly (exit 2). if (value === undefined || (value !== 'loopback' && isIP(value) === 0)) { error = { message: `shard: --trust-proxy must be 'loopback' or an IP literal, got: ${value ?? ''}`, code: 2 }; break; } trustedProxyUpstream = value; } else if (a.startsWith('--trust-proxy=')) { const value = a.slice('--trust-proxy='.length); if (value.length === 0 || (value !== 'loopback' && isIP(value) === 0)) { error = { message: `shard: --trust-proxy must be 'loopback' or an IP literal, got: ${value}`, code: 2 }; break; } trustedProxyUpstream = value; } else if (a === '--allow-insecure-remote') { allowInsecureRemote = true; } else if (a === '-h' || a === '--help') { help = true; } // Unknown args are silently skipped (historical daemon parser behavior). } const remoteOpts: DaemonOptions = { ...(webHost !== undefined ? { webHost } : {}), ...(webPort !== undefined ? { webPort } : {}), ...(publicHost !== undefined ? { publicHost } : {}), ...(allowedOrigins.length > 0 ? { allowedOrigins } : {}), ...(trustedProxyUpstream !== undefined ? { trustedProxy: { upstream: trustedProxyUpstream } } : {}), ...(allowInsecureRemote ? { allowInsecureRemote: true } : {}), }; return { ...(socketPath !== undefined ? { socketPath } : undefined), ...(home !== undefined ? { home } : undefined), ...(restartPolicy !== undefined ? { restartPolicy } : undefined), trustProject, remoteOpts, help, ...(error !== undefined ? { error } : undefined), }; } /** Info passed to the announce callback after the daemon boots. */ interface DaemonAnnounceInfo { socketPath: string; webPort?: number; /** Effective web bind host (for URL construction). */ webHost: string; /** * True when no non-revoked browser principal is enrolled yet (first run). The announce * callback uses this — together with inline approval — to decide whether the printed URL * should carry `?flow=approve`: only the first approval run needs it; an already-approved * operator gets the plain URL. Absent when the web surface is disabled. */ needsEnrollment?: boolean; } /** Options for {@link bootDaemon}. */ interface BootDaemonOpts { /** * When true, the daemon process listens for browser enrollment requests and prompts the * operator to approve them inline (readline y/n) in the same terminal — no separate * `shard web approve ` shell. Enabled for `setup --web` and for `shard web` when * stdin is a TTY (saga vr7kgx.19). */ inlineApproval?: boolean; } /** * Build a lazy `administer` closure bound to a daemon holder. The daemon is not yet * constructed when the enrollment callback is registered (it is created by `startDaemon` * one tick later), so we capture a mutable holder and read `.current` at call time. */ function makeAdminister(getDaemon: () => { administer?: (cmd: { type: 'approve'; code: string }) => Promise } | undefined) { return async (code: string): Promise => { const daemon = getDaemon(); if (daemon?.administer === undefined) throw new Error('daemon is not ready'); await daemon.administer({ type: 'approve', code }); }; } /** * Boot the daemon from parsed flags: validate, start, announce, then wire SIGINT/SIGTERM * for graceful shutdown. Runs until a signal arrives (returns a never-resolving promise). * The `announce` callback lets `shard daemon` print the socket and `shard web` print the * URL from the same boot path. */ async function bootDaemon( parsed: DaemonFlagResult, announce: (info: DaemonAnnounceInfo) => void, bootOpts?: BootDaemonOpts, ): Promise { if (parsed.restartPolicy !== undefined && !['never', 'always', 'on-crash'].includes(parsed.restartPolicy)) { process.stderr.write(`shard: invalid --restart policy: ${parsed.restartPolicy}\n`); return 2; } const { startDaemon, validateRemoteDaemonOptions } = await import('@/daemon'); try { validateRemoteDaemonOptions(parsed.remoteOpts); } catch (err) { process.stderr.write(`shard: ${(err as Error).message}\n`); return 1; } const webHost = parsed.remoteOpts.webHost ?? '127.0.0.1'; const inlineApproval = bootOpts?.inlineApproval ?? false; // The daemon is assigned after startDaemon resolves, but the enrollment callback is // registered DURING startDaemon. Capture a mutable holder so the closure reads the live // daemon when a browser actually connects (always after boot completes). let daemon: { address: string; stop(): Promise; administer?: (cmd: { type: 'approve'; code: string }) => Promise } | undefined; const administer = inlineApproval ? makeAdminister(() => daemon) : undefined; // Serialize prompts: skip a request that arrives while another is pending. let approvalPending = false; const onEnrollmentRequest = inlineApproval ? (enrollment: { code: string; expiresAt: number }): void => { if (approvalPending) return; approvalPending = true; void handleInlineApproval(enrollment, administer!).finally(() => { approvalPending = false; }); } : undefined; let webPort: number | undefined; let needsEnrollment = false; try { daemon = await startDaemon({ ...(parsed.socketPath !== undefined ? { socketPath: parsed.socketPath } : undefined), ...(parsed.home !== undefined ? { home: parsed.home } : undefined), ...(parsed.restartPolicy !== undefined ? { supervisor: { restart: parsed.restartPolicy as 'never' | 'always' | 'on-crash' } } : undefined), trustProject: parsed.trustProject, ...parsed.remoteOpts, ...(onEnrollmentRequest !== undefined ? { onEnrollmentRequest } : undefined), onReady: (info) => { webPort = info.webPort; if (info.webNeedsEnrollment !== undefined) needsEnrollment = info.webNeedsEnrollment; }, }); } catch (err) { process.stderr.write(`shard: failed to start daemon: ${(err as Error).message}\n`); return 1; } announce({ socketPath: daemon.address, webHost, ...(webPort !== undefined ? { webPort, needsEnrollment } : undefined), }); // Graceful shutdown on SIGINT/SIGTERM — the response path (`daemon.shutdown`) is for // clients; signals are for the operator. Stop is idempotent. let stopping = false; const stop = async (sig: NodeJS.Signals): Promise => { if (stopping) return; stopping = true; process.stderr.write(`\nshardd: ${sig} received, shutting down...\n`); try { await daemon!.stop(); } catch (e) { process.stderr.write(`shardd: shutdown error: ${(e as Error).message}\n`); } process.exit(0); }; process.on('SIGINT', (sig) => void stop(sig)); process.on('SIGTERM', (sig) => void stop(sig)); // Keep the process alive until a signal arrives. return new Promise(() => {}); } const DAEMON_HELP = 'Usage: shard daemon [--socket ] [--home ] [--restart ] [--trust-project]\n' + ' [--web-host ] [--web-port ] [--public-host ]\n' + ' [--allowed-origin ...] [--trust-proxy ] [--allow-insecure-remote]\n'; const WEB_HELP = 'Usage: shard web [options] start the daemon + browser client (prints URL)\n' + ' shard web web administration\n' + 'Admin: approve , clients, revoke , token create --name |rotate\n' + 'Options: [--socket ] [--home ] [--web-host ] [--web-port ]\n' + ' [--public-host ] [--allowed-origin ...]\n' + ' [--trust-proxy ] [--allow-insecure-remote] [--trust-project]\n'; /** * `shard daemon` / `shard shardd` — boot the long-lived multi-agent daemon (PROTOCOL §6/§8). * Parses `--socket ` / `--home ` overrides + optional `--restart ` and * runs until SIGINT/SIGTERM, then exits 0. No model resolution (the daemon spawns * `shard agent` children that resolve their own config). */ async function runDaemonSub(rest: string[]): Promise { const parsed = parseDaemonFlags(rest); if (parsed.help) { process.stderr.write(DAEMON_HELP); return 0; } if (parsed.error !== undefined) { process.stderr.write(`${parsed.error.message}\n`); return parsed.error.code; } return bootDaemon(parsed, (info) => { process.stderr.write(`shardd: listening on ${info.socketPath}\n`); }); } /** * `shard web` with no admin subcommand — start the localhost daemon + browser client and * print the URL. Accepts the same advanced daemon flags as `shard daemon`. Admin * subcommands (approve/clients/revoke/token) are routed separately in `main()` before * this is called, so they are preserved unchanged. * * When `inlineApproval` is set (saga vr7kgx.19), the daemon process watches for the first * browser enrollment request and prompts the operator to approve it inline — the printed * URL carries `?flow=approve` so the browser defaults to the approval-code flow and the * operator never needs a second terminal. The approval-flow link is shown ONLY for the * first approval run: once a browser is already enrolled (`needsEnrollment` is false) the * plain URL is printed instead, since no approval will be needed. */ async function runWebDaemon(rest: string[], opts?: BootDaemonOpts): Promise { const parsed = parseDaemonFlags(rest); if (parsed.help) { process.stderr.write(WEB_HELP); return 0; } if (parsed.error !== undefined) { process.stderr.write(`${parsed.error.message}\n`); return parsed.error.code; } const inlineApproval = opts?.inlineApproval ?? false; return bootDaemon(parsed, (info) => { if (info.webPort !== undefined) { const useApprovalFlow = inlineApproval && (info.needsEnrollment ?? false); const flow = useApprovalFlow ? '?flow=approve' : ''; process.stdout.write(`Shard web ready: http://${info.webHost}:${info.webPort}${flow}\n`); process.stderr.write(`shardd: listening on ${info.socketPath}\n`); if (useApprovalFlow) { process.stderr.write(`Open the URL in a browser — I'll prompt you here to approve the connection.\n`); } else { process.stderr.write(`Open the URL in a browser. Ctrl-C to stop.\n`); } } else { process.stderr.write(`shardd: listening on ${info.socketPath} (web disabled)\n`); } }, opts); } /** Route `shard models [args]` sub-subcommands. Returns exit code. */ async function runModelsSub(rest: string[]): Promise { const { runModelsList, runModelsShow } = await import('@/cli/commands/models-cmd'); const sub = rest[0]; const args = rest.slice(1); if (sub === 'list') { const { providerKey } = extractProviderFlag(args); return runModelsList({ ...(providerKey !== undefined ? { providerKey } : undefined) }); } if (sub === 'show') { const { providerKey, leftover } = extractProviderFlag(args); const alias = leftover[0]; if (alias === undefined) { process.stderr.write('Usage: shard models show [--provider p]\n'); return 1; } return runModelsShow(alias, { ...(providerKey !== undefined ? { providerKey } : undefined) }); } process.stderr.write('Usage: shard models [--provider p] [alias]\n'); return 1; } /** * Build per-model reasoning metadata for the {@link SessionModelController} (saga p7m8jp.2). * Resolves each catalog descriptor via `resolveCatalogModel` and extracts the supported * effort IDs + the configured default. Descriptors without `reasoningEfforts` get no entry * (no selectable effort surface). Secrets are never carried — only effort IDs and defaults. */ function buildReasoningMeta( models: ModelDescriptor[], config: ShardConfig, env: Record, resolveFn: typeof import('@/cli/config/model-catalog').resolveCatalogModel, ): Record { const meta: Record = {}; for (const descriptor of models) { try { const resolved = resolveFn(descriptor, config, env); if (resolved.reasoningEfforts !== undefined || resolved.reasoningEffort !== undefined) { meta[descriptor.id] = { ...(resolved.reasoningEfforts !== undefined ? { efforts: resolved.reasoningEfforts } : undefined), ...(resolved.reasoningEffort !== undefined ? { defaultEffort: resolved.reasoningEffort } : undefined), }; } } catch { // A descriptor that fails to resolve (e.g. auth env unset for a non-active provider) // simply has no reasoning metadata — it won't appear in the reasoning catalog. } } return meta; } /** Entry. */ async function main(): Promise { const argv = process.argv.slice(2); // Find the first subcommand (a bare positional that is NOT a flag value). We must skip // the value of any preceding value-taking flag so a flag value is not mistaken for the // subcommand. Subcommand may also be omitted (defaults to `web`). let sub: string | undefined; let restStart = 0; for (let i = 0; i < argv.length; i++) { const a = argv[i]; if (a === undefined) continue; if (a.startsWith('-')) { // If this flag takes a value, skip the next element. if (VALUE_FLAGS.has(a)) i++; continue; } sub = a; restStart = i + 1; break; } if (sub === undefined) { sub = 'web'; // default to the browser experience } else if (!SUBCOMMANDS.has(sub)) { process.stderr.write(`shard: unknown subcommand: ${sub}\n`); usageExit(); } const rest = argv.slice(restStart); // Commands that do not directly run an agent route before provider resolution. if (sub === 'new') { const { runNewSub } = await import('@/cli/commands/worktree-cmd'); process.exit(await runNewSub(rest)); } if (sub === 'worktree') { const { runWorktreeSub } = await import('@/cli/commands/worktree-cmd'); process.exit(await runWorktreeSub(rest)); } if (sub === 'config') { process.exit(await runConfigSub(rest)); } if (sub === 'models') { process.exit(await runModelsSub(rest)); } if (sub === 'web') { // Admin subcommands (approve, clients, revoke, token) route to the web admin client. // No admin subcommand → start the localhost daemon + browser client and print the URL. // We detect the admin path by the first bare positional (flags like --socket are // skipped) so `shard web --web-port 8787` starts the daemon while // `shard web token create --name x` routes to admin. const WEB_ADMIN_SUBS = new Set(['approve', 'clients', 'revoke', 'token']); const firstPositional = rest.find((a) => !a.startsWith('-')); if (firstPositional !== undefined && WEB_ADMIN_SUBS.has(firstPositional)) { const { runWebSub } = await import('@/cli/commands/web-cmd'); process.exit(await runWebSub(rest)); } process.exit(await runWebDaemon(rest, { inlineApproval: (process.stdin as { isTTY?: boolean }).isTTY ?? false })); } if (sub === 'setup') { const { runSetupSub } = await import('@/cli/commands/setup-cmd'); const code = await runSetupSub(rest); if (code !== 0 || !rest.includes('--web')) process.exit(code); process.stdout.write('\nStarting the local web experience…\n'); process.exit(await runWebDaemon([], { inlineApproval: true })); } if (sub === 'gc') { const { runGcSub } = await import('@/cli/commands/gc-cmd'); process.exit(await runGcSub(rest)); } if (sub === 'daemon' || sub === 'shardd') { process.exit(await runDaemonSub(rest)); } const opts = parseOptions(rest); // Build the wired core for the headless agent. Provider/ // model now flow through the config resolver (`@/cli/config`): loadConfig() reads // `~/.config/shard/config.json` (XDG), resolveModel() applies the 6-step override matrix // (flag > env > config). The `--api-key`/`--base-url` flags + `OPENAI_BASE_URL` env stay // as test-only escape hatches that override the resolver's output. Failure is loud: // resolveModel throws on any stuck case → exit 2 (matching the old `src/cli/index.ts:118` // precedent). const { loadConfig, resolveModel } = await import('@/cli/config'); let config; try { ({ config } = loadConfig()); } catch (err) { // Missing config, malformed JSON, or schema failure — fail loudly with exit 2. process.stderr.write(`${(err as Error).message}\n`); process.exit(2); } let resolvedModel: string; let resolvedProvider: string; let resolvedApiKey: string | undefined; let resolvedAuth: 'bearer' | 'none' = 'bearer'; let resolvedBaseURL: string | undefined; // saga hihsjg: wire-adapter + reasoning flags surfaced by the resolver so the provider // factory can pick OpenAIResponsesProvider vs OpenAIProvider. let resolvedApi: 'chat-completions' | 'responses' = 'chat-completions'; let resolvedReasoning = false; // saga p7m8jp.1: resolved reasoning effort (startup default; undefined → adapter default). let resolvedReasoningEffort: string | undefined; // saga u4uo9m.3 plan item 1: a daemon-managed subagent is spawned with // `--managed-manifest ` + `--daemon-socket `. Load + validate the manifest // BEFORE model resolution/bootstrap and treat its pinned values as AUTHORITATIVE — // managed model/provider/mode/cwd/prompt/tools/skills/depth/policy/timeout override // conflicting direct flags. Direct/test paths (no `--managed-manifest`) are unchanged. // Prompt/task/config never appear on argv (saga u4uo9m non-disclosure). let managedAuthority: import('@/cli/managed-authority').ManagedAuthority | undefined; // saga u4uo9m.5: the bundled subagent tools bootstrap contribution factory. Lazily // imported when a managed manifest is present; passed into `createDefaultCore` so the // canonical bootstrap installs the six tools through the ExtensionHost. let contributionFactory: import('@/contracts/bootstrap-contribution').BootstrapContributionFactory | undefined; if (opts.managedManifest !== undefined) { const { loadManifestFile } = await import('@/daemon/agent-catalog'); const manifest = loadManifestFile(opts.managedManifest); if (manifest === undefined) { process.stderr.write(`shard: managed manifest not found or invalid: ${opts.managedManifest}\n`); process.exit(2); } // A managed child connects back to the daemon over the supplied socket; the manifest's // pinned system prompt + capability policy drive the bootstrap (full authority). const daemonSocket = opts.daemonSocket; if (daemonSocket === undefined) { process.stderr.write('shard: --managed-manifest requires --daemon-socket\n'); process.exit(2); } const { buildManagedAuthority } = await import('@/cli/managed-authority'); managedAuthority = buildManagedAuthority(manifest, daemonSocket); // saga u4uo9m.5: the bundled subagent management tools install as a bootstrap // contribution (the factory is invoked by the canonical bootstrap with the trusted // agent-definition snapshot + the pinned managed context). Imported lazily so a // direct (non-managed) run pays no L3 import cost. contributionFactory = (await import('@/subagent/extension')).buildSubagentContribution(); } // Build the env snapshot passed to the (pure) resolver. `--api-key` is a test-only // escape hatch: when set, pre-populate EVERY bearer envVar in the catalog so the // resolver's auth step succeeds regardless of which provider resolves (then the // override below pins the final token to the flag value). Without this, the resolver // would throw on a missing auth env before the override could apply. const resolverEnv: Record = { ...process.env }; if (opts.apiKey !== undefined) { for (const p of Object.values(config.providers)) { if (p.auth.kind === 'bearer') resolverEnv[p.auth.envVar] = opts.apiKey; } } let resolvedContextWindow: number | undefined; try { // saga u4uo9m.3: managed manifest authority — pinned provider/model win over direct // flags (the manifest is loaded + validated above; the override falls back to the flag // when the manifest omits that field). const managedProvider = managedAuthority?.providerOverride ?? opts.providerKey; const managedModel = managedAuthority?.modelOverride ?? opts.modelName; const r = resolveModel({ config, env: resolverEnv, ...(managedProvider !== undefined ? { providerKey: managedProvider } : undefined), ...(managedModel !== undefined ? { modelName: managedModel } : undefined), }); resolvedModel = r.modelName; resolvedProvider = r.provider; resolvedBaseURL = r.baseURL; resolvedAuth = r.auth.kind; if (r.auth.kind === 'bearer') resolvedApiKey = r.auth.token; resolvedContextWindow = r.contextWindow; resolvedApi = r.api ?? 'chat-completions'; resolvedReasoning = r.reasoning === true; resolvedReasoningEffort = r.reasoningEffort; } catch (err) { process.stderr.write(`${(err as Error).message}\n`); process.exit(2); } // Escape-hatch overrides (test-only): explicit flags + `OPENAI_BASE_URL` env win over // the resolver's output, preserving the legacy env-override path for one-off runs. const apiKey = opts.apiKey ?? resolvedApiKey; const baseURL = opts.baseURL ?? process.env.OPENAI_BASE_URL ?? resolvedBaseURL; // saga dfcvo6: compaction is ON by default. Resolve the context window: // per-model override (r.contextWindow) → global config.compaction.contextWindow → 128k. // Then build the partial CompactionConfig (createDefaultCore applies the // reserveTokens/keepRecentTokens/enabled defaults). `enabled: false` opts out. const compactionContextWindow = resolvedContextWindow ?? config.compaction?.contextWindow ?? DEFAULT_CONTEXT_WINDOW; const compaction = { contextWindow: compactionContextWindow, ...(config.compaction?.enabled !== undefined ? { enabled: config.compaction.enabled } : undefined), ...(config.compaction?.reserveTokens !== undefined ? { reserveTokens: config.compaction.reserveTokens } : undefined), ...(config.compaction?.keepRecentTokens !== undefined ? { keepRecentTokens: config.compaction.keepRecentTokens } : undefined), }; const bootstrapExtensions: import('@/core/default').BootstrapExtension[] = []; if (config.codeIndex?.enabled !== false) { const { createCodeIndexExtension } = await import('@/codeindex/extension'); bootstrapExtensions.push( createCodeIndexExtension({ cwd: managedAuthority?.cwdOverride ?? opts.cwd }), ); } const core = await createDefaultCore({ model: resolvedModel, providerKey: resolvedProvider, auth: resolvedAuth, // saga hihsjg: surface the resolved wire adapter + reasoning flag. api: resolvedApi, ...(resolvedReasoning ? { reasoning: true } : undefined), // saga p7m8jp.1: carry the resolved reasoning effort through the bootstrap. ...(resolvedReasoningEffort !== undefined ? { reasoningEffort: resolvedReasoningEffort } : undefined), ...(apiKey !== undefined ? { apiKey } : undefined), ...(baseURL !== undefined ? { baseURL } : undefined), sessionPath: opts.sessionPath, ...(managedAuthority?.sessionIdOverride !== undefined ? { sessionId: managedAuthority.sessionIdOverride } : undefined), // saga u4uo9m.3: managed manifest authority — pinned cwd/agentId/mode win over flags. cwd: managedAuthority?.cwdOverride ?? opts.cwd, ...(managedAuthority?.agentIdOverride !== undefined ? { agentId: managedAuthority.agentIdOverride } : opts.agentId !== undefined ? { agentId: opts.agentId } : undefined), ...(managedAuthority?.modeOverride !== undefined ? { mode: managedAuthority.modeOverride } : undefined), // saga u4uo9m.3: pinned definition body is the base identity (restart/relaunch // determinism — same body/config regardless of source-file edits). ...(managedAuthority?.systemPrompt !== undefined ? { systemPrompt: managedAuthority.systemPrompt } : undefined), // saga u4uo9m.3: managed-child bootstrap restrictions — strict capability filtering, // fresh child RealPermissionGate (read-only-allow / mutate-ask), carried managed // context for later L3 extension injection. L2 never imports L3; this is plain data. ...(managedAuthority !== undefined ? { managed: managedAuthority.managed } : undefined), // saga u4uo9m.5 plan item 1: inject the bundled subagent management tools as a // bootstrap CONTRIBUTION FACTORY. The bootstrap calls it with the trusted // agent-definition snapshot (discovered below) + the pinned managed context, then // installs the six tools through the ExtensionHost before the capability snapshot — // so they are live and subject to the capability policy. L4 imports the L3 factory; // the core only calls the L1 function type (it never imports `@/subagent/**`). ...(managedAuthority !== undefined && contributionFactory !== undefined ? { contributionFactory } : undefined), // duoh76.7: opt-in permission gate. A `permissions` block in config.json // instantiates a RealPermissionGate sharing the core's bus/modeController. // Absent → AllowAllGate (no regression). Covered calls gate immediately; uncovered // calls reach `ask`, answerable once duoh76.10's command→event bridge ships. ...(config.permissions ? { permissions: config.permissions } : undefined), // dfcvo6: compaction ON by default (enabled defaults true in createDefaultCore). compaction, extensions: bootstrapExtensions, // d4ks8c.7: the canonical bootstrap discovers typed resources (skills/context/agents/ // rules) and trusted dynamic extensions through ExtensionApi. Project instruction/code // resources require affirmative trust; `projectTrusted` defaults to `false` (filtered, // no project code executed) until the CLI grows an explicit trust flag. resources: { cwd: opts.cwd, userHome: homedir(), projectTrusted: opts.trustProject, }, }); const session = createAgentSessionFromCore(core); if (sub === 'agent') { // saga u4uo9m.3: the managed manifest was already loaded + validated before model // resolution and its authority applied to the core above (pinned model/provider/mode/ // cwd/system_prompt + the managed bootstrap restrictions). No prompt/task/config ever // appears on argv. Build the per-session model catalog in the CLI layer. Descriptors // contain no auth material; provider construction resolves credentials only when a // selection is made. const { buildModelCatalog, resolveCatalogModel } = await import('@/cli/config/model-catalog'); const { SessionModelController } = await import('@/core/model-controller'); const { OpenAIProvider } = await import('@/core/provider/openai'); const { OpenAIResponsesProvider } = await import('@/core/provider/openai-responses'); const catalog = await buildModelCatalog({ config, env: resolverEnv, activeProvider: resolvedProvider, activeModel: resolvedModel, // saga vr7kgx.7: stamp the resolved compaction context window onto descriptors that // lack a per-alias override, so the web UI's context-size display has the same total the // compactor fires against (per-model → global → default). fallbackContextWindow: compactionContextWindow, }); // saga p7m8jp.2: one session-stable cache key reused across all Responses provider // reconstructions (model/effort switches) so prefix caching is not lost. const modelControllerCacheKey = crypto.randomUUID(); const modelController = new SessionModelController({ models: catalog.models, active: catalog.active, queueProvider: (provider) => session.queueProvider(provider), // saga p7m8jp.2: per-model reasoning metadata (efforts + default) keyed by descriptor ID. reasoning: buildReasoningMeta(catalog.models, config, resolverEnv, resolveCatalogModel), // saga p7m8jp.2: session-stable cache key reused across Responses provider reconstructions. promptCacheKey: crypto.randomUUID(), createProvider: (descriptor, effort) => { const resolved = resolveCatalogModel(descriptor, config, resolverEnv); const common = { model: resolved.modelName, modelDescriptor: descriptor, baseURL: resolved.baseURL, auth: resolved.auth.kind, ...(resolved.auth.kind === 'bearer' ? { apiKey: resolved.auth.token } : {}), } as const; // saga p7m8jp.2: the `effort` parameter is the controller's authoritative active value // (the model's default on a switch, or the new value on an effort change). For // Responses, always pass it (the provider defaults to `medium` when undefined). For // Chat Completions, send it ONLY when the model declared `reasoningEfforts` // (explicit opt-in); generic endpoints receive no new field by default. const responsesEffortOpt = effort !== undefined ? { reasoningEffort: effort } : undefined; const chatEffortOpt = effort !== undefined && resolved.reasoningEfforts !== undefined ? { reasoningEffort: effort } : undefined; // saga hihsjg: branch on the resolved wire adapter. return resolved.api === 'responses' ? new OpenAIResponsesProvider({ ...common, ...(resolved.reasoning ? { reasoning: true } : undefined), ...responsesEffortOpt, // saga p7m8jp.2: reuse the session-stable cache key. ...(modelControllerCacheKey !== undefined ? { promptCacheKey: modelControllerCacheKey } : undefined), }) : new OpenAIProvider({ ...common, ...chatEffortOpt, }); }, }); // Stdio headless server speaking the per-agent protocol (PROTOCOL v0.1, §2.1/§5.2/§9). // `shard agent [--id ]` boots a direct-connection stdio server over the same wired // pieces. `--socket ` is RESERVED for the daemon-owned unix-socket mode (build // step 6); this subcommand speaks stdio only in step 5. const { runStdioAgentServer } = await import('@/transports/stdio'); try { await runStdioAgentServer({ session, modeController: core.modeController, sessionStore: core.sessionStore, eventBus: core.eventBus, agentId: core.agentId, modelController, writerLease: { runtimeGeneration: opts.runtimeGeneration ?? `direct-${process.pid}`, daemonLease: opts.daemonLease ?? 'direct-stdio', }, // saga u4uo9m.3 plan item 7: lease-loss (stdin EOF) disposes extensions (in-flight // dialogs unblock) and the production forced-exit safety net routes through // `process.exit` so a hung keepalive cannot pin a leased child. dispose: () => core.host.dispose(), exit: (code) => process.exit(code), }); } finally { await core.host.dispose(); } return; } // Unreachable: SUBCOMMANDS guard above. usageExit(); } await main();