diff --git a/docs/architecture.md b/docs/architecture.md index 0990939..7008764 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -805,9 +805,10 @@ retry reset, candidate approval, or export is triggered by capture or replay. ### Local unknown Claude category capture -The Anthropic user-session adapter accepts an optional -`localClaudeCategoryCaptureParent` for explicit diagnostic sessions. Capture is -disabled by default, has no CLI or renderer control, and requires a +The local application driver accepts an optional +`localClaudeCategoryCaptureParent` for explicit diagnostic sessions and passes +it only to Anthropic user-session adapters constructed for run execution. +Capture is disabled by default, has no CLI or renderer control, and requires a caller-selected parent directory that already exists. When a structured Claude error contains an unrecognized category-shaped `subtype` or `terminal_reason`, the adapter writes only those exact strings to a new `categories.json` file in diff --git a/docs/roadmap.md b/docs/roadmap.md index fb19406..065c546 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -764,6 +764,13 @@ private material remain excluded; durable diagnostics record only fixed capture-saved or capture-failed codes. This provider-free diagnostic does not reinterpret #384 or authorize another live attempt. +Issue #395 routes that explicit capture parent through the local application +run boundary only when a diagnostic caller supplies it. Default workflows, +API-key adapters, OpenAI user sessions, CLI and renderer controls, persisted +workspace configuration, and provider behavior remain unchanged. This plumbing +makes #393 usable by a later bounded diagnostic workflow but neither performs +nor authorizes provider transmission. + The twenty-second bounded observation under #382 used revision `349388de4820eca30543492d8ad1266199cdda3e` after explicit provider-transmission authorization. Both 20-second authentication probes passed, but three @@ -886,6 +893,7 @@ issues retain implementation chronology. | Date | Decision | Product implication | | ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 2026-09-15 | Routed opt-in Claude category capture through the local run boundary under #395. | A programmatic diagnostic caller can pass the private capture parent to Anthropic user-session run adapters without enabling capture for default, API-key, OpenAI, CLI, renderer, or persisted workspace paths. This provider-free plumbing does not authorize a live attempt. | | 2026-09-14 | Added opt-in local capture for unknown Claude categories under #393. | Explicit diagnostic sessions can preserve only bounded, category-shaped unknown result subtype and terminal-reason strings in a private caller-owned file. Capture is disabled by default, durable history remains content-free, provider behavior is unchanged, and no live attempt is authorized. | | 2026-09-13 | Recorded #384 as an indeterminate twenty-third matched-backend observation. | After explicit authorization, both 20-second authentication probes passed, but one Anthropic author attempt failed non-retryably with `unknown` after 351,508 ms. Fixed diagnostics classify only result subtype, terminal reason, and stop reason; no artifact or review occurred, and authorization is exhausted. This adds no product-quality evidence; #75/#250 remain blocked. | | 2026-09-13 | Recorded #382 as an indeterminate twenty-second matched-backend observation. | After explicit authorization, both 20-second auth probes passed, but three Anthropic author attempts failed with generic `invalid-response`; the first two were retryable and the final attempt was non-retryable with factual-invariant rejection. No artifact or review occurred; authorization is exhausted and #75/#250 remain blocked. | diff --git a/packages/application/src/local-claude-category-capture.test.ts b/packages/application/src/local-claude-category-capture.test.ts new file mode 100644 index 0000000..f630201 --- /dev/null +++ b/packages/application/src/local-claude-category-capture.test.ts @@ -0,0 +1,279 @@ +import { mkdir, mkdtemp, readdir, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +import type { + AnthropicClient, + OpenAIClient, + UserSessionProcessRunner, +} from "@draft-loop/providers"; +import { openSqliteStorage } from "@draft-loop/storage"; +import { describe, expect, it, vi } from "vitest"; + +import { createLocalApplicationDriver } from "./local.js"; + +const silent = { write: () => undefined }; + +async function providerWorkspace(prefix: string): Promise { + const root = await mkdtemp(join(tmpdir(), prefix)); + await mkdir(join(root, "evidence")); + await writeFile(join(root, "job.md"), "Build reliable TypeScript tools.\n", "utf8"); + await writeFile(join(root, "evidence", "resume.md"), "Built TypeScript tools.\n", "utf8"); + return root; +} + +async function initializeWorkspace( + root: string, + authorCompany: "anthropic" | "openai", + criticCompany: "anthropic" | "openai", +): Promise { + await createLocalApplicationDriver().initialize( + { + root, + jobDescription: "job.md", + sources: "evidence", + authorCompany, + authorModel: authorCompany === "anthropic" ? "claude-sonnet-4-5" : "gpt-5.6-luna", + criticCompany, + criticModel: criticCompany === "anthropic" ? "claude-sonnet-4-5" : "gpt-5.6-luna", + }, + silent, + ); +} + +function claudeUnknownCategoryRunner( + subtype: string, + terminalReason: string, +): UserSessionProcessRunner { + return vi.fn(async () => ({ + exitCode: 1, + stdout: JSON.stringify({ + type: "result", + is_error: true, + subtype, + terminal_reason: terminalReason, + stop_reason: "stop_sequence", + api_error_status: 503, + result: "synthetic-private-result-marker", + errors: ["synthetic-private-errors-marker"], + session_id: "synthetic-private-session-marker", + usage: { private: "synthetic-private-usage-marker" }, + structured_output: { private: "synthetic-private-structured-output-marker" }, + prompt: "synthetic-private-prompt-marker", + path: "/synthetic/private/path-marker", + api_key: "synthetic-secret-credential-marker", + }), + stderr: "synthetic-private-stderr-marker", + })); +} + +describe("local Claude category capture routing", () => { + it("routes only unknown Anthropic user-session categories to the configured local parent", async () => { + const root = await providerWorkspace("draft-loop-app-claude-capture-"); + const captureParent = await mkdtemp(join(tmpdir(), "draft-loop-app-claude-capture-parent-")); + const subtype = "future_application_error_category"; + const terminalReason = "future_application_terminal_reason"; + const runner = claudeUnknownCategoryRunner(subtype, terminalReason); + const output: string[] = []; + const io = { write: (line: string) => output.push(line) }; + const driver = createLocalApplicationDriver({ + localClaudeCategoryCaptureParent: captureParent, + providerAuthModeConfiguration: { anthropic: "user-session", openai: "api-key" }, + userSessionRunners: { anthropic: runner }, + resolveCredential: async () => "synthetic-secret-credential-marker", + }); + + try { + await initializeWorkspace(root, "anthropic", "openai"); + const snapshot = await driver.start({ root, allowProviderData: true }, io); + + expect(snapshot).toMatchObject({ + state: "provider-error", + lastError: { + code: "transient", + message: "The provider request failed. You can retry safely.", + provider: "anthropic", + step: "author", + retryable: true, + }, + }); + expect(snapshot.lastError?.diagnostics).toContainEqual({ + code: "local_claude_category_capture_saved", + path: "local_claude_category_capture", + }); + expect(runner).toHaveBeenCalledOnce(); + + const directories = await readdir(captureParent); + expect(directories).toHaveLength(1); + const directoryName = directories[0]; + if (directoryName === undefined) throw new Error("Expected a category capture directory."); + const captureDirectory = join(captureParent, directoryName); + const captureText = await readFile(join(captureDirectory, "categories.json"), "utf8"); + expect(JSON.parse(captureText)).toEqual({ subtype, terminal_reason: terminalReason }); + + const storage = openSqliteStorage(join(root, ".draft-loop", "history.sqlite")); + try { + const history = { + run: await storage.getRun(snapshot.runId), + events: await storage.listAuditEvents(snapshot.workspaceId), + }; + const durableText = JSON.stringify({ snapshot, history, output }); + for (const marker of [ + subtype, + terminalReason, + captureParent, + directoryName, + "synthetic-private-result-marker", + "synthetic-private-errors-marker", + "synthetic-private-session-marker", + "synthetic-private-usage-marker", + "synthetic-private-structured-output-marker", + "synthetic-private-prompt-marker", + "/synthetic/private/path-marker", + "synthetic-secret-credential-marker", + "synthetic-private-stderr-marker", + ]) { + expect(durableText).not.toContain(marker); + } + } finally { + await storage.close(); + } + } finally { + await rm(root, { recursive: true, force: true }); + await rm(captureParent, { recursive: true, force: true }); + } + }); + + it("leaves capture disabled by default for an Anthropic user session", async () => { + const root = await providerWorkspace("draft-loop-app-claude-capture-default-"); + const subtype = "future_default_category_marker"; + const runner = claudeUnknownCategoryRunner(subtype, "future_default_terminal"); + const output: string[] = []; + + try { + await initializeWorkspace(root, "anthropic", "openai"); + const snapshot = await createLocalApplicationDriver({ + providerAuthModeConfiguration: { anthropic: "user-session", openai: "api-key" }, + userSessionRunners: { anthropic: runner }, + }).start({ root, allowProviderData: true }, { write: (line) => output.push(line) }); + + expect(snapshot).toMatchObject({ + state: "provider-error", + lastError: { + code: "transient", + step: "author", + retryable: true, + }, + }); + expect(snapshot.lastError?.diagnostics).toContainEqual({ + code: "claude_error_subtype_unrecognized", + path: "subtype", + }); + expect(snapshot.lastError?.diagnostics).not.toContainEqual( + expect.objectContaining({ + code: expect.stringMatching(/^local_claude_category_capture_/u), + }), + ); + expect(runner).toHaveBeenCalledOnce(); + expect(JSON.stringify({ snapshot, output })).not.toContain(subtype); + expect(JSON.stringify({ snapshot, output })).not.toContain("future_default_terminal"); + } finally { + await rm(root, { recursive: true, force: true }); + } + }); + + it("does not create capture output for Anthropic API-key authentication", async () => { + const root = await providerWorkspace("draft-loop-app-anthropic-api-key-capture-"); + const captureParent = await mkdtemp(join(tmpdir(), "draft-loop-app-anthropic-api-key-parent-")); + const output: string[] = []; + const create = vi.fn(async () => { + throw new Error("synthetic Anthropic API-key failure"); + }); + const credentials = vi.fn(async () => "synthetic-anthropic-api-key"); + const driver = createLocalApplicationDriver({ + localClaudeCategoryCaptureParent: captureParent, + providerAuthModeConfiguration: { anthropic: "api-key", openai: "api-key" }, + resolveCredential: credentials, + providerClientFactories: { + anthropic: () => ({ messages: { create } }) as unknown as AnthropicClient, + }, + }); + + try { + await initializeWorkspace(root, "anthropic", "openai"); + const snapshot = await driver.start( + { root, allowProviderData: true }, + { write: (line) => output.push(line) }, + ); + + expect(snapshot).toMatchObject({ + state: "provider-error", + lastError: { provider: "anthropic", step: "author" }, + }); + expect(credentials).toHaveBeenCalledOnce(); + expect(credentials).toHaveBeenCalledWith("anthropic"); + expect(create).toHaveBeenCalledOnce(); + expect(await readdir(captureParent)).toEqual([]); + expect(JSON.stringify({ snapshot, output })).not.toContain(captureParent); + } finally { + await rm(root, { recursive: true, force: true }); + await rm(captureParent, { recursive: true, force: true }); + } + }); + + it.each(["user-session", "api-key"] as const)( + "does not route capture through the OpenAI %s adapter", + async (authMode) => { + const root = await providerWorkspace(`draft-loop-app-openai-capture-${authMode}-`); + const captureParent = await mkdtemp(join(tmpdir(), "draft-loop-app-openai-capture-parent-")); + const output: string[] = []; + const driver = createLocalApplicationDriver({ + localClaudeCategoryCaptureParent: captureParent, + providerAuthModeConfiguration: { anthropic: "api-key", openai: authMode }, + resolveCredential: async () => "synthetic-openai-api-key", + ...(authMode === "user-session" + ? { + userSessionRunners: { + openai: vi.fn(async () => ({ + exitCode: 1, + stdout: "synthetic OpenAI user-session failure", + stderr: "", + })), + }, + } + : { + providerClientFactories: { + openai: () => + ({ + responses: { + create: async () => { + throw new Error("synthetic OpenAI API-key failure"); + }, + }, + }) as OpenAIClient, + }, + }), + }); + + try { + await initializeWorkspace(root, "openai", "anthropic"); + const snapshot = await driver.start( + { root, allowProviderData: true }, + { + write: (line) => output.push(line), + }, + ); + + expect(snapshot).toMatchObject({ + state: "provider-error", + lastError: { provider: "openai", step: "author" }, + }); + expect(await readdir(captureParent)).toEqual([]); + expect(JSON.stringify({ snapshot, output })).not.toContain(captureParent); + } finally { + await rm(root, { recursive: true, force: true }); + await rm(captureParent, { recursive: true, force: true }); + } + }, + ); +}); diff --git a/packages/application/src/local-provider-adapter.ts b/packages/application/src/local-provider-adapter.ts new file mode 100644 index 0000000..045dc50 --- /dev/null +++ b/packages/application/src/local-provider-adapter.ts @@ -0,0 +1,129 @@ +import Anthropic from "@anthropic-ai/sdk"; +import type { ModelSelection } from "@draft-loop/domain"; +import { + AnthropicAdapter, + AnthropicClaudeUserSessionAdapter, + type AnthropicClient, + type JsonObject, + type LocalClient, + LocalModelAdapter, + OpenAIAdapter, + type OpenAIClient, + OpenAICodexUserSessionAdapter, + ProviderAdapterError, + type UserSessionProcessRunner, +} from "@draft-loop/providers"; +import OpenAI from "openai"; + +/** Concrete local driver shared by CLI and the native desktop host. */ +export type ProviderCredentialResolver = ( + provider: "anthropic" | "openai", +) => Promise; + +export interface ProviderClientFactories { + readonly anthropic?: (apiKey: string) => AnthropicClient; + readonly openai?: (apiKey: string) => OpenAIClient; + /** + * Builds the local transport. Receives the workspace's configured endpoint, + * or `undefined` when the workspace leaves the adapter default in place. + */ + readonly local?: (endpoint: string | undefined) => LocalClient; +} + +export interface ProviderUserSessionRunners { + readonly anthropic?: UserSessionProcessRunner; + readonly openai?: UserSessionProcessRunner; +} + +export type { AnthropicClient, LocalClient, OpenAIClient }; + +type ProviderAuthModeConfiguration = Readonly< + Record<"anthropic" | "openai", "api-key" | "user-session"> +>; + +/** Resolve the literal transport company; model lineage remains a separate concern. */ +function providerId(company: string): "anthropic" | "openai" | "local" { + if (company === "anthropic" || company === "openai" || company === "local") return company; + throw new ProviderAdapterError( + "anthropic", + "invalid-request", + "The workspace provider configuration is unsupported.", + { retryable: false }, + ); +} + +export async function createProviderAdapter( + config: { readonly localEndpoint?: string }, + model: ModelSelection, + allowProviderData: boolean, + resolveCredential: ProviderCredentialResolver, + providerClientFactories?: ProviderClientFactories, + providerAuthModeConfiguration: ProviderAuthModeConfiguration = { + anthropic: "api-key", + openai: "api-key", + }, + userSessionRunners?: ProviderUserSessionRunners, + userSessionTimeoutMs?: number, + localClaudeCategoryCaptureParent?: string, +) { + const provider = providerId(model.company); + if (!allowProviderData) { + throw new ProviderAdapterError( + provider, + "policy", + "Provider transmission is not approved for this request.", + { retryable: false }, + ); + } + if (provider === "local") { + const client: LocalClient = + providerClientFactories?.local?.(config.localEndpoint) ?? + (config.localEndpoint === undefined ? {} : { endpoint: config.localEndpoint }); + return new LocalModelAdapter(client, { configuredModel: model }); + } + if (provider === "anthropic") { + if (providerAuthModeConfiguration.anthropic === "user-session") { + return new AnthropicClaudeUserSessionAdapter({ + configuredModel: model, + ...(userSessionRunners?.anthropic === undefined + ? {} + : { runner: userSessionRunners.anthropic }), + ...(userSessionTimeoutMs === undefined ? {} : { timeoutMs: userSessionTimeoutMs }), + ...(localClaudeCategoryCaptureParent === undefined + ? {} + : { localClaudeCategoryCaptureParent }), + }); + } + const apiKey = await resolveCredential("anthropic"); + if (apiKey === undefined || apiKey.trim() === "") { + throw new ProviderAdapterError( + provider, + "authentication", + "The provider credential is not configured.", + { retryable: false }, + ); + } + const client = + providerClientFactories?.anthropic?.(apiKey) ?? + (new Anthropic({ apiKey, maxRetries: 0 }) as unknown as AnthropicClient); + return new AnthropicAdapter(client, { configuredModel: model }); + } + if (providerAuthModeConfiguration.openai === "user-session") { + return new OpenAICodexUserSessionAdapter({ + configuredModel: model, + ...(userSessionRunners?.openai === undefined ? {} : { runner: userSessionRunners.openai }), + ...(userSessionTimeoutMs === undefined ? {} : { timeoutMs: userSessionTimeoutMs }), + }); + } + const apiKey = await resolveCredential("openai"); + if (apiKey === undefined || apiKey.trim() === "") { + throw new ProviderAdapterError( + provider, + "authentication", + "The provider credential is not configured.", + { retryable: false }, + ); + } + const client = providerClientFactories?.openai?.(apiKey) ?? new OpenAI({ apiKey, maxRetries: 0 }); + return new OpenAIAdapter(client, { configuredModel: model }); +} diff --git a/packages/application/src/local.ts b/packages/application/src/local.ts index d9ade2d..53631d4 100644 --- a/packages/application/src/local.ts +++ b/packages/application/src/local.ts @@ -1,7 +1,6 @@ import { createHash, randomUUID } from "node:crypto"; import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises"; import { basename, dirname, extname, isAbsolute, join, relative, resolve } from "node:path"; -import Anthropic from "@anthropic-ai/sdk"; import { createArtifact, createArtifactVersion, @@ -50,19 +49,10 @@ import { type RunSnapshot, } from "@draft-loop/orchestrator"; import { - AnthropicAdapter, - AnthropicClaudeUserSessionAdapter, - type AnthropicClient, type JsonObject, - type LocalClient, - LocalModelAdapter, type ModelRequest, type ModelResponse, - OpenAIAdapter, - type OpenAIClient, - OpenAICodexUserSessionAdapter, ProviderAdapterError, - type UserSessionProcessRunner, } from "@draft-loop/providers"; import { extensionForFormat, @@ -88,7 +78,6 @@ import { type WorkspaceRecord, type WritingPolicyVersionRecord, } from "@draft-loop/storage"; -import OpenAI from "openai"; import { createAuthorAdjudicationPrompt } from "./author-adjudication.js"; import { createAuthorGroundingGuide } from "./author-grounding.js"; import { @@ -131,6 +120,12 @@ import { } from "./knowledge-base.js"; import { requestLocalAdjudicatedRevision } from "./local-adjudicated-revision.js"; import { defaultLocalModelEndpoint, isLoopbackEndpoint } from "./local-endpoint.js"; +import type { + ProviderClientFactories, + ProviderCredentialResolver, + ProviderUserSessionRunners, +} from "./local-provider-adapter.js"; +import { createProviderAdapter } from "./local-provider-adapter.js"; import { localJobRequirements } from "./local-requirements.js"; import { saveTypedHistory } from "./local-typed-history.js"; import type { @@ -144,6 +139,15 @@ import { buildAuthorArtifactWithCapture } from "./rejected-author-capture.js"; import { createRequirementAchievementPlan } from "./requirement-achievement-plan.js"; import { responseExecution, timestamp } from "./response-execution.js"; +export type { + AnthropicClient, + LocalClient, + OpenAIClient, + ProviderClientFactories, + ProviderCredentialResolver, + ProviderUserSessionRunners, +} from "./local-provider-adapter.js"; + const configDirectory = ".draft-loop"; const configFilename = "workspace.json"; const databaseFilename = "history.sqlite"; @@ -1978,88 +1982,6 @@ function providerDataPolicy( }; } -/** Resolve the literal transport company; model lineage remains a separate concern. */ -function providerId(company: string): "anthropic" | "openai" | "local" { - if (company === "anthropic" || company === "openai" || company === "local") return company; - throw new ProviderAdapterError( - "anthropic", - "invalid-request", - "The workspace provider configuration is unsupported.", - { retryable: false }, - ); -} - -async function createProviderAdapter( - config: WorkspaceConfig, - model: ModelSelection, - allowProviderData: boolean, - resolveCredential: ProviderCredentialResolver, - providerClientFactories?: ProviderClientFactories, - providerAuthModeConfiguration: ProviderAuthModeConfiguration = { - anthropic: "api-key", - openai: "api-key", - }, - userSessionRunners?: ProviderUserSessionRunners, - userSessionTimeoutMs?: number, -) { - const provider = providerId(model.company); - if (!allowProviderData) { - throw new ProviderAdapterError( - provider, - "policy", - "Provider transmission is not approved for this request.", - { retryable: false }, - ); - } - if (provider === "local") { - const client: LocalClient = - providerClientFactories?.local?.(config.localEndpoint) ?? - (config.localEndpoint === undefined ? {} : { endpoint: config.localEndpoint }); - return new LocalModelAdapter(client, { configuredModel: model }); - } - if (provider === "anthropic") { - if (providerAuthModeConfiguration.anthropic === "user-session") { - return new AnthropicClaudeUserSessionAdapter({ - configuredModel: model, - ...(userSessionRunners?.anthropic === undefined - ? {} - : { runner: userSessionRunners.anthropic }), - ...(userSessionTimeoutMs === undefined ? {} : { timeoutMs: userSessionTimeoutMs }), - }); - } - const apiKey = await resolveCredential("anthropic"); - if (apiKey === undefined || apiKey.trim() === "") { - throw new ProviderAdapterError( - provider, - "authentication", - "The provider credential is not configured.", - { retryable: false }, - ); - } - const client = - providerClientFactories?.anthropic?.(apiKey) ?? - (new Anthropic({ apiKey, maxRetries: 0 }) as unknown as AnthropicClient); - return new AnthropicAdapter(client, { configuredModel: model }); - } - if (providerAuthModeConfiguration.openai === "user-session") { - return new OpenAICodexUserSessionAdapter({ - configuredModel: model, - ...(userSessionRunners?.openai === undefined ? {} : { runner: userSessionRunners.openai }), - ...(userSessionTimeoutMs === undefined ? {} : { timeoutMs: userSessionTimeoutMs }), - }); - } - const apiKey = await resolveCredential("openai"); - if (apiKey === undefined || apiKey.trim() === "") { - throw new ProviderAdapterError( - provider, - "authentication", - "The provider credential is not configured.", - { retryable: false }, - ); - } - const client = providerClientFactories?.openai?.(apiKey) ?? new OpenAI({ apiKey, maxRetries: 0 }); - return new OpenAIAdapter(client, { configuredModel: model }); -} function providerAgents( config: WorkspaceConfig, context: ContextSnapshot, @@ -2073,6 +1995,7 @@ function providerAgents( userSessionRunners?: ProviderUserSessionRunners, userSessionTimeoutMs?: number, authorProposalCaptureDirectory?: string, + localClaudeCategoryCaptureParent?: string, ): { readonly author: AuthorAgent; readonly critic: CriticAgent } { const dataPolicy = (company: string) => providerDataPolicy(company, allowProviderData, providerAuthModeConfiguration); @@ -2086,6 +2009,7 @@ function providerAgents( providerAuthModeConfiguration, userSessionRunners, userSessionTimeoutMs, + localClaudeCategoryCaptureParent, ); } const promptContext = modelFacingContext(context); @@ -2232,6 +2156,7 @@ function engine( userSessionTimeoutMs?: number, retrieval?: RetrievalPort, authorProposalCaptureDirectory?: string, + localClaudeCategoryCaptureParent?: string, ): OrchestrationEngine { const agents = needsAgents ? config.fixtureMode @@ -2246,6 +2171,7 @@ function engine( userSessionRunners, userSessionTimeoutMs, authorProposalCaptureDirectory, + localClaudeCategoryCaptureParent, ) : noopAgents(); const store = createStorageRunStore(storage); @@ -2399,6 +2325,7 @@ interface RunOptions { readonly userSessionTimeoutMs?: number; readonly signal?: AbortSignal; readonly authorProposalCaptureDirectory?: string; + readonly localClaudeCategoryCaptureParent?: string; } type OmitRunOptions = Omit; type BeginStartRunOptions = OmitRunOptions<"runId" | "signal" | "writingPolicyOverrideChecksum">; @@ -2532,6 +2459,7 @@ async function createRun( options.userSessionTimeoutMs, candidateRetrieval?.port, options.authorProposalCaptureDirectory, + options.localClaudeCategoryCaptureParent, ); const request = { runId, @@ -2599,6 +2527,7 @@ export async function resumeRun( options.userSessionTimeoutMs, candidateRetrieval?.port, options.authorProposalCaptureDirectory, + options.localClaudeCategoryCaptureParent, ); preflight(config, io, budget(config)); const snapshot = await runEngine.resume(runId, { @@ -3152,28 +3081,6 @@ export async function inspectWorkspaceEvidenceRetrieval( } } -/** Concrete local driver shared by CLI and the native desktop host. */ -export type ProviderCredentialResolver = ( - provider: "anthropic" | "openai", -) => Promise; - -export interface ProviderClientFactories { - readonly anthropic?: (apiKey: string) => AnthropicClient; - readonly openai?: (apiKey: string) => OpenAIClient; - /** - * Builds the local transport. Receives the workspace's configured endpoint, - * or `undefined` when the workspace leaves the adapter default in place. - */ - readonly local?: (endpoint: string | undefined) => LocalClient; -} - -export interface ProviderUserSessionRunners { - readonly anthropic?: UserSessionProcessRunner; - readonly openai?: UserSessionProcessRunner; -} - -export type { AnthropicClient, LocalClient, OpenAIClient }; - export interface LocalApplicationDriverOptions { readonly providerAuthMode?: ProviderAuthMode; readonly providerAuthModeConfiguration?: ProviderAuthModeConfiguration; @@ -3182,6 +3089,7 @@ export interface LocalApplicationDriverOptions { readonly userSessionRunners?: ProviderUserSessionRunners; readonly userSessionTimeoutMs?: number; readonly authorProposalCaptureDirectory?: string; + readonly localClaudeCategoryCaptureParent?: string; } const environmentCredentialResolver: ProviderCredentialResolver = async (provider) => @@ -3332,6 +3240,12 @@ export function createLocalApplicationDriver( ...providerClientOptions, ...authOptions, }; + const runProviderOptions = { + ...providerOpportunityOptions, + ...(options?.localClaudeCategoryCaptureParent === undefined + ? {} + : { localClaudeCategoryCaptureParent: options.localClaudeCategoryCaptureParent }), + }; return { initialize: async (command, io) => await workspaceDescriptor(resolve(command.root), await initWorkspace(command, io)), @@ -3369,9 +3283,7 @@ export function createLocalApplicationDriver( ...(command.writingPolicyOverrideChecksum === undefined ? {} : { writingPolicyOverrideChecksum: command.writingPolicyOverrideChecksum }), - ...credentialOptions, - ...providerClientOptions, - ...authOptions, + ...runProviderOptions, }, io, ), @@ -3391,9 +3303,7 @@ export function createLocalApplicationDriver( ...(command.writingPolicyOverrideChecksum === undefined ? {} : { writingPolicyOverrideChecksum: command.writingPolicyOverrideChecksum }), - ...credentialOptions, - ...providerClientOptions, - ...authOptions, + ...runProviderOptions, }, io, ), @@ -3406,9 +3316,7 @@ export function createLocalApplicationDriver( ? {} : { allowProviderData: command.allowProviderData }), ...(command.signal === undefined ? {} : { signal: command.signal }), - ...credentialOptions, - ...providerClientOptions, - ...authOptions, + ...runProviderOptions, }, io, ), diff --git a/scripts/architecture-hotspots.mjs b/scripts/architecture-hotspots.mjs index e66465b..1fc16a4 100644 --- a/scripts/architecture-hotspots.mjs +++ b/scripts/architecture-hotspots.mjs @@ -5,7 +5,7 @@ import { pathToFileURL } from "node:url"; export const hotspotLineLimits = Object.freeze({ "packages/application/src/knowledge-base.ts": 6_168, - "packages/application/src/local.ts": 3_780, + "packages/application/src/local.ts": 3_688, "packages/domain/src/index.ts": 5_693, "packages/schemas/src/index.ts": 4_936, "packages/storage/src/index.ts": 14_938,