@posthog/wizard 2.84.0 → 2.85.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{agent-interface-BNssNetq.js → agent-interface-C2yoWWW_.js} +18 -11
- package/dist/agent-interface-C2yoWWW_.js.map +1 -0
- package/dist/{agent-prompt-loader-iV11XVEF.js → agent-prompt-loader-B6ZyyoSH.js} +6 -6
- package/dist/{agent-prompt-loader-iV11XVEF.js.map → agent-prompt-loader-B6ZyyoSH.js.map} +1 -1
- package/dist/analytics-DM5Fx9JF.js +2 -0
- package/dist/{analytics-Drox5LSW.js → analytics-J1cjIekQ.js} +2 -2
- package/dist/{analytics-Drox5LSW.js.map → analytics-J1cjIekQ.js.map} +1 -1
- package/dist/{api-Dfu655Qt.js → api-DoMocc6P.js} +2 -2
- package/dist/{api-Dfu655Qt.js.map → api-DoMocc6P.js.map} +1 -1
- package/dist/api-bUx3g0Jj.js +2 -0
- package/dist/{api-key-login-BgwfAXfv.js → api-key-login-CjXOunDa.js} +5 -5
- package/dist/{api-key-login-BgwfAXfv.js.map → api-key-login-CjXOunDa.js.map} +1 -1
- package/dist/api-key-login-D4Esj8TZ.js +2 -0
- package/dist/bin.js +1 -1
- package/dist/{bounded-fs-TRL6IsoU.js → bounded-fs-Dr1DVwWs.js} +5 -4
- package/dist/bounded-fs-Dr1DVwWs.js.map +1 -0
- package/dist/{ci-install-nc1IKuX2.js → ci-install-ByYDF-nS.js} +4 -4
- package/dist/{ci-install-nc1IKuX2.js.map → ci-install-ByYDF-nS.js.map} +1 -1
- package/dist/{debug-BfO_Txo8.js → debug-BWrfAT_d.js} +3 -3
- package/dist/{debug-BfO_Txo8.js.map → debug-BWrfAT_d.js.map} +1 -1
- package/dist/{gateway-CLQQb96F.js → gateway-DrKvPnHW.js} +9 -6
- package/dist/{gateway-CLQQb96F.js.map → gateway-DrKvPnHW.js.map} +1 -1
- package/dist/{headless-DMO8N_FD.js → headless-B3NomAmg.js} +2 -2
- package/dist/{headless-DMO8N_FD.js.map → headless-B3NomAmg.js.map} +1 -1
- package/dist/{headless-CHGNEzO3.js → headless-D2lM4BeY.js} +1 -1
- package/dist/{host-resolution-DftfSjJD.js → host-resolution-BAnXpvmo.js} +4 -4
- package/dist/{host-resolution-DftfSjJD.js.map → host-resolution-BAnXpvmo.js.map} +1 -1
- package/dist/{install-MWoo1hXL.js → install-BaZS9PMi.js} +1 -1
- package/dist/{install-C9FPM37m.js → install-x8br-QvS.js} +5 -5
- package/dist/{install-C9FPM37m.js.map → install-x8br-QvS.js.map} +1 -1
- package/dist/{interactive-DOnolJHB.js → interactive-D0X4iQOs.js} +3 -3
- package/dist/{interactive-DOnolJHB.js.map → interactive-D0X4iQOs.js.map} +1 -1
- package/dist/{links-CatXPRxH.js → links-DCUnJ0Fl.js} +3 -3
- package/dist/{links-CatXPRxH.js.map → links-DCUnJ0Fl.js.map} +1 -1
- package/dist/{main-CINmZtoW.js → main-CCIvlpq6.js} +20 -20
- package/dist/{main-CINmZtoW.js.map → main-CCIvlpq6.js.map} +1 -1
- package/dist/{mcp-KVV6Z_bq.js → mcp-D1rpXjM2.js} +3 -3
- package/dist/{mcp-KVV6Z_bq.js.map → mcp-D1rpXjM2.js.map} +1 -1
- package/dist/{mcp-prompt-streaming-M6UnLgGX.js → mcp-prompt-streaming-GXBA581m.js} +5 -5
- package/dist/{mcp-prompt-streaming-M6UnLgGX.js.map → mcp-prompt-streaming-GXBA581m.js.map} +1 -1
- package/dist/{models-B0huHItG.js → models-HNKiMsME.js} +2 -2
- package/dist/{models-B0huHItG.js.map → models-HNKiMsME.js.map} +1 -1
- package/dist/{oauth-session-9EU3ky0v.js → oauth-session-DTydKnfV.js} +2 -2
- package/dist/{oauth-session-9EU3ky0v.js.map → oauth-session-DTydKnfV.js.map} +1 -1
- package/dist/{orchestrator-tools-QWyDLu_7.js → orchestrator-tools-a3uMYekz.js} +3 -3
- package/dist/{orchestrator-tools-QWyDLu_7.js.map → orchestrator-tools-a3uMYekz.js.map} +1 -1
- package/dist/{package-manager-B-9I_fAT.js → package-manager-D_APgur1.js} +4 -4
- package/dist/{package-manager-B-9I_fAT.js.map → package-manager-D_APgur1.js.map} +1 -1
- package/dist/{pi-BSnSk6rv.js → pi-BXwrltc4.js} +12 -12
- package/dist/{pi-BSnSk6rv.js.map → pi-BXwrltc4.js.map} +1 -1
- package/dist/{playground-Drz1qszs.js → playground-ZiIX53Cx.js} +4 -4
- package/dist/{playground-Drz1qszs.js.map → playground-ZiIX53Cx.js.map} +1 -1
- package/dist/{posthog-integration-mIQGBc1f.js → posthog-integration-yK9EktZn.js} +21 -21
- package/dist/{posthog-integration-mIQGBc1f.js.map → posthog-integration-yK9EktZn.js.map} +1 -1
- package/dist/{programs-mxhRXwP-.js → programs-AZStBYjR.js} +19 -15
- package/dist/programs-AZStBYjR.js.map +1 -0
- package/dist/programs-DrvCAQHg.js +3 -0
- package/dist/{provisioning-CHUjae9V.js → provisioning-BpP-DrUL.js} +4 -4
- package/dist/{provisioning-CHUjae9V.js.map → provisioning-BpP-DrUL.js.map} +1 -1
- package/dist/provisioning-D8ZhYNCc.js +2 -0
- package/dist/{queue-tools-PFG6H93Q.js → queue-tools-BS8nXJ6S.js} +4 -4
- package/dist/{queue-tools-PFG6H93Q.js.map → queue-tools-BS8nXJ6S.js.map} +1 -1
- package/dist/{results-5yCKf2cI.js → results-BE1ctlKP.js} +2 -2
- package/dist/{results-5yCKf2cI.js.map → results-BE1ctlKP.js.map} +1 -1
- package/dist/{run-W03b6jNM.js → run-Cmn1xaWK.js} +10 -10
- package/dist/{run-W03b6jNM.js.map → run-Cmn1xaWK.js.map} +1 -1
- package/dist/{run-D5TKJuSL.js → run-D0AZVe7z.js} +7 -7
- package/dist/{run-D5TKJuSL.js.map → run-D0AZVe7z.js.map} +1 -1
- package/dist/{run-tool-DTQF8mmI.js → run-tool-DAt35gkq.js} +13 -13
- package/dist/{run-tool-DTQF8mmI.js.map → run-tool-DAt35gkq.js.map} +1 -1
- package/dist/run-tool-IHvPCuQT.js +2 -0
- package/dist/{runners-B_84kPyf.js → runners-BsTLloGF.js} +8 -8
- package/dist/{runners-B_84kPyf.js.map → runners-BsTLloGF.js.map} +1 -1
- package/dist/{security-pApVawYb.js → security-CQZUg-Oz.js} +5 -5
- package/dist/{security-pApVawYb.js.map → security-CQZUg-Oz.js.map} +1 -1
- package/dist/{setup-utils-CQY8PHjR.js → setup-utils-CqITBPrn.js} +3 -3
- package/dist/{setup-utils-CQY8PHjR.js.map → setup-utils-CqITBPrn.js.map} +1 -1
- package/dist/{start-playground-DAw1GbLt.js → start-playground-CUhA5g6G.js} +7 -7
- package/dist/{start-playground-DAw1GbLt.js.map → start-playground-CUhA5g6G.js.map} +1 -1
- package/dist/{subagent-YLGSvgoZ.js → subagent-BPynEn_q.js} +3 -3
- package/dist/{subagent-YLGSvgoZ.js.map → subagent-BPynEn_q.js.map} +1 -1
- package/dist/{task-3hRSovmK.js → task-Cuwyq3HT.js} +14 -14
- package/dist/{task-3hRSovmK.js.map → task-Cuwyq3HT.js.map} +1 -1
- package/dist/{telemetry-BPBzqsb_.js → telemetry-BoTjlaAw.js} +2 -2
- package/dist/{telemetry-BPBzqsb_.js.map → telemetry-BoTjlaAw.js.map} +1 -1
- package/dist/{terminal-DZsYFbm9.js → terminal-CmCNn0IG.js} +18 -18
- package/dist/{terminal-DZsYFbm9.js.map → terminal-CmCNn0IG.js.map} +1 -1
- package/dist/{tools-D_AtrhbO.js → tools-Blf-Yjru.js} +13 -13
- package/dist/{tools-D_AtrhbO.js.map → tools-Blf-Yjru.js.map} +1 -1
- package/dist/{tools-DyzRi_dQ.js → tools-DvefdnQx.js} +8 -8
- package/dist/{tools-DyzRi_dQ.js.map → tools-DvefdnQx.js.map} +1 -1
- package/dist/{tui-UI6ctBQJ.js → tui-B_nBr0aF.js} +1 -1
- package/dist/{tui-CRp0iCuT.js → tui-CKbAdFxQ.js} +4 -4
- package/dist/{tui-CRp0iCuT.js.map → tui-CKbAdFxQ.js.map} +1 -1
- package/dist/{upload-step-DrYXnpzI.js → upload-step-Dqb7Dzfn.js} +3 -3
- package/dist/{upload-step-DrYXnpzI.js.map → upload-step-Dqb7Dzfn.js.map} +1 -1
- package/dist/{urls-WeqfBLP4.js → urls-DWZ9EKLI.js} +2 -2
- package/dist/{urls-WeqfBLP4.js.map → urls-DWZ9EKLI.js.map} +1 -1
- package/dist/{wizard-abort-BwEefm6P.js → wizard-abort-g6PYcdlP.js} +4 -4
- package/dist/{wizard-abort-BwEefm6P.js.map → wizard-abort-g6PYcdlP.js.map} +1 -1
- package/dist/{yara-hooks-MDtFbuoM.js → yara-hooks-BsOgyb6-.js} +1 -1
- package/dist/{yara-hooks-aDbdsitJ.js → yara-hooks-ssWAT78l.js} +4 -4
- package/dist/{yara-hooks-aDbdsitJ.js.map → yara-hooks-ssWAT78l.js.map} +1 -1
- package/package.json +6 -1
- package/dist/agent-interface-BNssNetq.js.map +0 -1
- package/dist/analytics-Ix9HEra3.js +0 -2
- package/dist/api-H2UaPgjv.js +0 -2
- package/dist/api-key-login-Cow3UDAN.js +0 -2
- package/dist/bounded-fs-TRL6IsoU.js.map +0 -1
- package/dist/programs-Bv0_YHzE.js +0 -3
- package/dist/programs-mxhRXwP-.js.map +0 -1
- package/dist/provisioning-BVsxSNve.js +0 -2
- package/dist/run-tool-DjERhu7G.js +0 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { o as logToFile } from "./debug-
|
|
2
|
-
import { F as POSTHOG_EU_CLIENT_ID, N as POSTHOG_DEV_CLIENT_ID, V as POSTHOG_US_CLIENT_ID, et as WIZARD_PROVISIONING_SCOPES, it as WIZARD_USER_AGENT, n as analytics } from "./analytics-
|
|
3
|
-
import { s as resolveBaseUrl } from "./urls-
|
|
1
|
+
import { o as logToFile } from "./debug-BWrfAT_d.js";
|
|
2
|
+
import { F as POSTHOG_EU_CLIENT_ID, N as POSTHOG_DEV_CLIENT_ID, V as POSTHOG_US_CLIENT_ID, et as WIZARD_PROVISIONING_SCOPES, it as WIZARD_USER_AGENT, n as analytics } from "./analytics-J1cjIekQ.js";
|
|
3
|
+
import { s as resolveBaseUrl } from "./urls-DWZ9EKLI.js";
|
|
4
4
|
import axios from "axios";
|
|
5
5
|
import * as crypto from "node:crypto";
|
|
6
6
|
import { z } from "zod";
|
|
@@ -292,4 +292,4 @@ async function requestDeepLink(accessToken, host, opts) {
|
|
|
292
292
|
//#endregion
|
|
293
293
|
export { provisionNewAccount as n, requestDeepLink as r, ProvisionedAccountUnreadableError as t };
|
|
294
294
|
|
|
295
|
-
//# sourceMappingURL=provisioning-
|
|
295
|
+
//# sourceMappingURL=provisioning-BpP-DrUL.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"provisioning-CHUjae9V.js","names":[],"sources":["../src/shared/utils/provisioning.ts"],"sourcesContent":["/**\n * Provisioning API client for creating new PostHog accounts.\n *\n * Uses the agentic provisioning API with PKCE auth:\n * 1. POST /account_requests - create account, get auth code\n * 2. POST /oauth/token - exchange code for tokens (with PKCE)\n * 3. POST /resources - provision project, get API key\n */\n\nimport * as crypto from 'node:crypto';\nimport axios from 'axios';\nimport { z } from 'zod';\nimport {\n POSTHOG_DEV_CLIENT_ID,\n POSTHOG_EU_CLIENT_ID,\n POSTHOG_US_CLIENT_ID,\n WIZARD_PROVISIONING_SCOPES,\n WIZARD_USER_AGENT,\n} from '@shared/constants';\nimport { resolveBaseUrl } from './urls';\nimport { logToFile } from './debug';\nimport { analytics } from './analytics';\nimport type { HostResolution } from '@shared/host-resolution';\n\nconst API_VERSION = '0.1d';\n\n/**\n * Provisioning host. Follows a `--base-url` override (and IS_DEV → localhost),\n * else the prod provisioning host for the target region. Unlike the login OAuth\n * flow (which goes through the region-agnostic `oauth.posthog.com` proxy), the\n * provisioning API is region-specific: an EU account must be created against\n * `eu.posthog.com`, and the subsequent token-exchange / resources calls stay on\n * the same host (they carry the account's bearer token).\n */\nconst getProvisioningBaseUrl = (\n region: 'US' | 'EU',\n baseUrl?: string,\n): string => {\n const override = resolveBaseUrl(baseUrl);\n if (override) return override;\n return region === 'EU' ? 'https://eu.posthog.com' : 'https://us.posthog.com';\n};\n\n/**\n * OAuth client ID for provisioning. A pinned base URL means a dev-seeded stack\n * that registers the dev client; prod uses the client registered for the target\n * region (the wizard OAuth app is registered separately per region).\n *\n * TODO: same assumption as `getOAuthClientId` in oauth.ts — a pinned base URL is\n * treated as a dev-seeded instance. Make configurable if we ever point\n * `--base-url` at a non-dev instance with its own OAuth app.\n */\nconst getProvisioningClientId = (\n region: 'US' | 'EU',\n baseUrl?: string,\n): string => {\n if (resolveBaseUrl(baseUrl)) return POSTHOG_DEV_CLIENT_ID;\n return region === 'EU' ? POSTHOG_EU_CLIENT_ID : POSTHOG_US_CLIENT_ID;\n};\n\nfunction generateCodeVerifier(): string {\n return crypto.randomBytes(32).toString('base64url');\n}\n\nfunction generateCodeChallenge(verifier: string): string {\n return crypto.createHash('sha256').update(verifier).digest('base64url');\n}\n\n// --- Response schemas ---\n//\n// These schemas validate only the fields the wizard actually reads. The rule is load\n// bearing, not stylistic: the wizard ships to npm and old versions keep running against\n// a provisioning API that moves independently, so a field we require but never consume\n// is a crash we cannot fix in the field. `service_id` was exactly that — required here,\n// read nowhere, and every signup broke the day the API stopped echoing it. Zod strips\n// unknown keys, so fields the API adds are tolerated for free; the only way to break is\n// to demand something back.\n\nconst AccountRequestResponseSchema = z.object({\n id: z.string(),\n type: z.enum(['oauth', 'requires_auth', 'error']),\n oauth: z\n .object({\n code: z.string(),\n })\n .optional(),\n error: z\n .object({\n code: z.string(),\n message: z.string(),\n })\n .optional(),\n});\n\nconst TokenResponseSchema = z.object({\n token_type: z.string(),\n access_token: z.string(),\n refresh_token: z.string(),\n expires_in: z.number(),\n account: z\n .object({\n id: z.string(),\n })\n .optional(),\n});\n\nconst AccessConfigurationSchema = z.object({\n api_key: z.string(),\n host: z.string(),\n personal_api_key: z.string().optional(),\n});\n\nconst ResourceResponseSchema = z.object({\n status: z.string(),\n id: z.string(),\n complete: z\n .object({\n access_configuration: AccessConfigurationSchema,\n })\n .optional(),\n});\n\n/**\n * Teams the account can reach, from the token exchange. Parsed separately from\n * `TokenResponseSchema` and as loosely as possible: it is only used to recover from a\n * response we already failed to read, so it must never become a new way to fail.\n */\nconst AvailableTeamsSchema = z.array(\n z.object({ id: z.union([z.number(), z.string()]) }),\n);\n\n/**\n * Thrown when the provisioning API completed the work but the wizard could not read the\n * project back out of the response. The account, organization and project exist — callers\n * must not tell the user their account wasn't created.\n */\nexport class ProvisionedAccountUnreadableError extends Error {\n readonly accountCreated = true;\n\n constructor(message: string) {\n super(message);\n this.name = 'ProvisionedAccountUnreadableError';\n }\n}\n\n/** The field paths Zod rejected — never the values, which carry tokens and API keys. */\nfunction issuePaths(error: z.ZodError): string[] {\n return error.issues.map((issue) => issue.path.join('.') || '(root)');\n}\n\n/**\n * Report a response the wizard couldn't read. Silently degrading on a shape mismatch\n * turns the next contract change into a week of guesswork, so the rejected paths go to\n * the debug log and to error tracking before anything else happens.\n */\nfunction reportUnreadableResponse(step: string, error: z.ZodError): Error {\n const paths = issuePaths(error);\n const reported = new Error(\n `Unexpected ${step} response from the provisioning API (missing or invalid: ${paths.join(\n ', ',\n )})`,\n );\n\n logToFile(\n `[provisioning] unexpected ${step} response shape: ${paths.join(', ')}`,\n );\n analytics.captureException(reported, {\n step: `provisioning_${step}`,\n issue_paths: paths,\n });\n\n return reported;\n}\n\nfunction parseOrThrow<T extends z.ZodTypeAny>(\n schema: T,\n data: unknown,\n step: string,\n): z.infer<T> {\n const parsed = schema.safeParse(data);\n if (!parsed.success) {\n throw reportUnreadableResponse(step, parsed.error);\n }\n return parsed.data;\n}\n\ntype ResourceRead =\n | {\n ok: true;\n projectId: string;\n access: z.infer<typeof AccessConfigurationSchema>;\n }\n | { ok: false; reason: 'unreadable' | 'incomplete' };\n\nfunction readResource(data: unknown, step: string): ResourceRead {\n const parsed = ResourceResponseSchema.safeParse(data);\n if (!parsed.success) {\n reportUnreadableResponse(step, parsed.error);\n return { ok: false, reason: 'unreadable' };\n }\n\n if (parsed.data.status !== 'complete' || !parsed.data.complete) {\n logToFile(`[provisioning] ${step} returned status=${parsed.data.status}`);\n return { ok: false, reason: 'incomplete' };\n }\n\n return {\n ok: true,\n projectId: parsed.data.id,\n access: parsed.data.complete.access_configuration,\n };\n}\n\n/**\n * Recover the project after an unreadable `POST /resources` response.\n *\n * By the time that call returns 2xx the account, organization and project exist, so\n * aborting would abandon a provisioned account and leave the user with an orphaned\n * project. `GET /resources/:id` returns the same access configuration, so read it back\n * instead. Best-effort by design: any failure returns null and the caller reports the\n * original drift. The detail endpoint never mints a personal API key, so a recovered\n * result has none — `personalApiKey` is optional for every consumer.\n */\nasync function readBackResource(\n provisioningBaseUrl: string,\n accessToken: string,\n tokenPayload: unknown,\n): Promise<ResourceRead | null> {\n const teams = AvailableTeamsSchema.safeParse(\n (tokenPayload as { account?: { available_teams?: unknown } } | undefined)\n ?.account?.available_teams,\n );\n\n // A freshly provisioned account owns exactly one team. With any other count we can't\n // tell which team the create call made, and guessing would capture into the wrong one.\n if (!teams.success || teams.data.length !== 1) {\n logToFile(\n '[provisioning] no unambiguous team to read the resource back from',\n );\n return null;\n }\n\n const resourceId = String(teams.data[0].id);\n\n try {\n const res = await axios.get(\n `${provisioningBaseUrl}/api/agentic/provisioning/resources/${resourceId}`,\n {\n headers: {\n Authorization: `Bearer ${accessToken}`,\n 'API-Version': API_VERSION,\n 'User-Agent': WIZARD_USER_AGENT,\n },\n timeout: 30_000,\n },\n );\n\n logToFile(\n `[provisioning] read resource ${resourceId} back after unreadable create`,\n );\n return readResource(res.data, 'resource_readback');\n } catch {\n logToFile(`[provisioning] read-back of resource ${resourceId} failed`);\n return null;\n }\n}\n\nexport interface ProvisioningResult {\n accessToken: string;\n refreshToken: string;\n /** Epoch ms when `accessToken` expires. */\n expiresAt: number;\n /** OAuth client the grant was minted under — refreshes must present the same one. */\n oauthClientId: string;\n projectApiKey: string;\n host: string;\n personalApiKey?: string;\n projectId: string;\n accountId: string;\n}\n\n/**\n * Create a new PostHog account and provision a project via the provisioning API.\n *\n * This is the \"no browser\" signup path: the wizard collects the email,\n * calls the provisioning API to create the account, and gets back\n * credentials without opening a browser.\n */\nexport async function provisionNewAccount(\n email: string,\n name: string,\n region: 'US' | 'EU' = 'US',\n opts?: {\n orgName?: string;\n projectName?: string;\n baseUrl?: string;\n /** Scope list to request; defaults to `WIZARD_PROVISIONING_SCOPES`. */\n scopes?: readonly string[];\n },\n): Promise<ProvisioningResult> {\n const codeVerifier = generateCodeVerifier();\n const codeChallenge = generateCodeChallenge(codeVerifier);\n const provisioningBaseUrl = getProvisioningBaseUrl(region, opts?.baseUrl);\n const oauthClientId = getProvisioningClientId(region, opts?.baseUrl);\n\n logToFile('[provisioning] starting account creation');\n\n // Step 1: Create account\n const accountRes = await axios.post(\n `${provisioningBaseUrl}/api/agentic/provisioning/account_requests`,\n {\n id: crypto.randomUUID(),\n email,\n name,\n client_id: oauthClientId,\n code_challenge: codeChallenge,\n code_challenge_method: 'S256',\n scopes: opts?.scopes ?? WIZARD_PROVISIONING_SCOPES,\n configuration: {\n region,\n ...(opts?.orgName ? { organization_name: opts.orgName } : {}),\n },\n },\n {\n headers: {\n 'Content-Type': 'application/json',\n 'API-Version': API_VERSION,\n 'User-Agent': WIZARD_USER_AGENT,\n },\n timeout: 30_000,\n },\n );\n\n const accountData = parseOrThrow(\n AccountRequestResponseSchema,\n accountRes.data,\n 'account_requests',\n );\n\n if (accountData.type === 'error') {\n const msg = accountData.error?.message ?? 'Account creation failed';\n analytics.captureException(new Error(msg), {\n step: 'provisioning_account_request',\n error_code: accountData.error?.code,\n });\n throw new Error(msg);\n }\n\n if (accountData.type === 'requires_auth') {\n throw new Error(\n 'This email is already associated with a PostHog account. Please use the login flow instead.',\n );\n }\n\n const code = accountData.oauth?.code;\n if (!code) {\n throw new Error('No authorization code received from account creation');\n }\n\n logToFile('[provisioning] account created, exchanging code for tokens');\n\n // Step 2: Exchange code for tokens\n const tokenRes = await axios.post(\n `${provisioningBaseUrl}/api/agentic/oauth/token`,\n new URLSearchParams({\n grant_type: 'authorization_code',\n code,\n code_verifier: codeVerifier,\n }).toString(),\n {\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded',\n 'API-Version': API_VERSION,\n 'User-Agent': WIZARD_USER_AGENT,\n },\n timeout: 30_000,\n },\n );\n\n const tokenData = parseOrThrow(\n TokenResponseSchema,\n tokenRes.data,\n 'oauth_token',\n );\n\n logToFile('[provisioning] tokens received, provisioning resources');\n\n // Step 3: Provision resources\n const resourceRes = await axios.post(\n `${provisioningBaseUrl}/api/agentic/provisioning/resources`,\n {\n ...(opts?.projectName\n ? { configuration: { project_name: opts.projectName } }\n : {}),\n },\n {\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${tokenData.access_token}`,\n 'API-Version': API_VERSION,\n 'User-Agent': WIZARD_USER_AGENT,\n },\n timeout: 30_000,\n },\n );\n\n // `/resources` is synchronous: it answers `status: \"complete\"` or an error envelope.\n // So an unreadable body means the contract moved, not that provisioning is still\n // running — and the project already exists either way, which is why this recovers\n // instead of aborting.\n let resource = readResource(resourceRes.data, 'resources');\n\n if (!resource.ok && resource.reason === 'unreadable') {\n resource =\n (await readBackResource(\n provisioningBaseUrl,\n tokenData.access_token,\n tokenRes.data,\n )) ?? resource;\n }\n\n if (!resource.ok) {\n if (resource.reason === 'incomplete') {\n throw new Error('Resource provisioning did not complete');\n }\n throw new ProvisionedAccountUnreadableError(\n 'Your PostHog project was created, but the provisioning API returned a response the wizard could not read',\n );\n }\n\n logToFile('[provisioning] resources provisioned successfully');\n\n return {\n accessToken: tokenData.access_token,\n refreshToken: tokenData.refresh_token,\n expiresAt: Date.now() + tokenData.expires_in * 1000,\n oauthClientId,\n projectApiKey: resource.access.api_key,\n host: resource.access.host,\n personalApiKey: resource.access.personal_api_key,\n projectId: resource.projectId,\n accountId: tokenData.account?.id ?? '',\n };\n}\n\n/**\n * Request a one-time deep link URL that logs the user into PostHog\n * and redirects to their project dashboard — or, when `opts.path` is\n * given, to any safe in-app relative path (e.g. the Signals inbox).\n */\nexport async function requestDeepLink(\n accessToken: string,\n host: HostResolution,\n opts?: { purpose?: string; path?: string },\n): Promise<string | null> {\n try {\n const baseUrl = host.appHost;\n\n const res = await axios.post(\n `${baseUrl}/api/agentic/provisioning/deep_links`,\n {\n purpose: opts?.purpose ?? 'dashboard',\n ...(opts?.path ? { path: opts.path } : {}),\n },\n {\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${accessToken}`,\n 'API-Version': API_VERSION,\n 'User-Agent': WIZARD_USER_AGENT,\n },\n timeout: 10_000,\n },\n );\n\n const url = res.data?.url;\n if (typeof url === 'string') {\n logToFile(`[provisioning] deep link created: ${url}`);\n return url;\n }\n return null;\n } catch {\n logToFile('[provisioning] deep link request failed, skipping');\n return null;\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"provisioning-BpP-DrUL.js","names":[],"sources":["../src/shared/utils/provisioning.ts"],"sourcesContent":["/**\n * Provisioning API client for creating new PostHog accounts.\n *\n * Uses the agentic provisioning API with PKCE auth:\n * 1. POST /account_requests - create account, get auth code\n * 2. POST /oauth/token - exchange code for tokens (with PKCE)\n * 3. POST /resources - provision project, get API key\n */\n\nimport * as crypto from 'node:crypto';\nimport axios from 'axios';\nimport { z } from 'zod';\nimport {\n POSTHOG_DEV_CLIENT_ID,\n POSTHOG_EU_CLIENT_ID,\n POSTHOG_US_CLIENT_ID,\n WIZARD_PROVISIONING_SCOPES,\n WIZARD_USER_AGENT,\n} from '@shared/constants';\nimport { resolveBaseUrl } from './urls';\nimport { logToFile } from './debug';\nimport { analytics } from './analytics';\nimport type { HostResolution } from '@shared/host-resolution';\n\nconst API_VERSION = '0.1d';\n\n/**\n * Provisioning host. Follows a `--base-url` override (and IS_DEV → localhost),\n * else the prod provisioning host for the target region. Unlike the login OAuth\n * flow (which goes through the region-agnostic `oauth.posthog.com` proxy), the\n * provisioning API is region-specific: an EU account must be created against\n * `eu.posthog.com`, and the subsequent token-exchange / resources calls stay on\n * the same host (they carry the account's bearer token).\n */\nconst getProvisioningBaseUrl = (\n region: 'US' | 'EU',\n baseUrl?: string,\n): string => {\n const override = resolveBaseUrl(baseUrl);\n if (override) return override;\n return region === 'EU' ? 'https://eu.posthog.com' : 'https://us.posthog.com';\n};\n\n/**\n * OAuth client ID for provisioning. A pinned base URL means a dev-seeded stack\n * that registers the dev client; prod uses the client registered for the target\n * region (the wizard OAuth app is registered separately per region).\n *\n * TODO: same assumption as `getOAuthClientId` in oauth.ts — a pinned base URL is\n * treated as a dev-seeded instance. Make configurable if we ever point\n * `--base-url` at a non-dev instance with its own OAuth app.\n */\nconst getProvisioningClientId = (\n region: 'US' | 'EU',\n baseUrl?: string,\n): string => {\n if (resolveBaseUrl(baseUrl)) return POSTHOG_DEV_CLIENT_ID;\n return region === 'EU' ? POSTHOG_EU_CLIENT_ID : POSTHOG_US_CLIENT_ID;\n};\n\nfunction generateCodeVerifier(): string {\n return crypto.randomBytes(32).toString('base64url');\n}\n\nfunction generateCodeChallenge(verifier: string): string {\n return crypto.createHash('sha256').update(verifier).digest('base64url');\n}\n\n// --- Response schemas ---\n//\n// These schemas validate only the fields the wizard actually reads. The rule is load\n// bearing, not stylistic: the wizard ships to npm and old versions keep running against\n// a provisioning API that moves independently, so a field we require but never consume\n// is a crash we cannot fix in the field. `service_id` was exactly that — required here,\n// read nowhere, and every signup broke the day the API stopped echoing it. Zod strips\n// unknown keys, so fields the API adds are tolerated for free; the only way to break is\n// to demand something back.\n\nconst AccountRequestResponseSchema = z.object({\n id: z.string(),\n type: z.enum(['oauth', 'requires_auth', 'error']),\n oauth: z\n .object({\n code: z.string(),\n })\n .optional(),\n error: z\n .object({\n code: z.string(),\n message: z.string(),\n })\n .optional(),\n});\n\nconst TokenResponseSchema = z.object({\n token_type: z.string(),\n access_token: z.string(),\n refresh_token: z.string(),\n expires_in: z.number(),\n account: z\n .object({\n id: z.string(),\n })\n .optional(),\n});\n\nconst AccessConfigurationSchema = z.object({\n api_key: z.string(),\n host: z.string(),\n personal_api_key: z.string().optional(),\n});\n\nconst ResourceResponseSchema = z.object({\n status: z.string(),\n id: z.string(),\n complete: z\n .object({\n access_configuration: AccessConfigurationSchema,\n })\n .optional(),\n});\n\n/**\n * Teams the account can reach, from the token exchange. Parsed separately from\n * `TokenResponseSchema` and as loosely as possible: it is only used to recover from a\n * response we already failed to read, so it must never become a new way to fail.\n */\nconst AvailableTeamsSchema = z.array(\n z.object({ id: z.union([z.number(), z.string()]) }),\n);\n\n/**\n * Thrown when the provisioning API completed the work but the wizard could not read the\n * project back out of the response. The account, organization and project exist — callers\n * must not tell the user their account wasn't created.\n */\nexport class ProvisionedAccountUnreadableError extends Error {\n readonly accountCreated = true;\n\n constructor(message: string) {\n super(message);\n this.name = 'ProvisionedAccountUnreadableError';\n }\n}\n\n/** The field paths Zod rejected — never the values, which carry tokens and API keys. */\nfunction issuePaths(error: z.ZodError): string[] {\n return error.issues.map((issue) => issue.path.join('.') || '(root)');\n}\n\n/**\n * Report a response the wizard couldn't read. Silently degrading on a shape mismatch\n * turns the next contract change into a week of guesswork, so the rejected paths go to\n * the debug log and to error tracking before anything else happens.\n */\nfunction reportUnreadableResponse(step: string, error: z.ZodError): Error {\n const paths = issuePaths(error);\n const reported = new Error(\n `Unexpected ${step} response from the provisioning API (missing or invalid: ${paths.join(\n ', ',\n )})`,\n );\n\n logToFile(\n `[provisioning] unexpected ${step} response shape: ${paths.join(', ')}`,\n );\n analytics.captureException(reported, {\n step: `provisioning_${step}`,\n issue_paths: paths,\n });\n\n return reported;\n}\n\nfunction parseOrThrow<T extends z.ZodTypeAny>(\n schema: T,\n data: unknown,\n step: string,\n): z.infer<T> {\n const parsed = schema.safeParse(data);\n if (!parsed.success) {\n throw reportUnreadableResponse(step, parsed.error);\n }\n return parsed.data;\n}\n\ntype ResourceRead =\n | {\n ok: true;\n projectId: string;\n access: z.infer<typeof AccessConfigurationSchema>;\n }\n | { ok: false; reason: 'unreadable' | 'incomplete' };\n\nfunction readResource(data: unknown, step: string): ResourceRead {\n const parsed = ResourceResponseSchema.safeParse(data);\n if (!parsed.success) {\n reportUnreadableResponse(step, parsed.error);\n return { ok: false, reason: 'unreadable' };\n }\n\n if (parsed.data.status !== 'complete' || !parsed.data.complete) {\n logToFile(`[provisioning] ${step} returned status=${parsed.data.status}`);\n return { ok: false, reason: 'incomplete' };\n }\n\n return {\n ok: true,\n projectId: parsed.data.id,\n access: parsed.data.complete.access_configuration,\n };\n}\n\n/**\n * Recover the project after an unreadable `POST /resources` response.\n *\n * By the time that call returns 2xx the account, organization and project exist, so\n * aborting would abandon a provisioned account and leave the user with an orphaned\n * project. `GET /resources/:id` returns the same access configuration, so read it back\n * instead. Best-effort by design: any failure returns null and the caller reports the\n * original drift. The detail endpoint never mints a personal API key, so a recovered\n * result has none — `personalApiKey` is optional for every consumer.\n */\nasync function readBackResource(\n provisioningBaseUrl: string,\n accessToken: string,\n tokenPayload: unknown,\n): Promise<ResourceRead | null> {\n const teams = AvailableTeamsSchema.safeParse(\n (tokenPayload as { account?: { available_teams?: unknown } } | undefined)\n ?.account?.available_teams,\n );\n\n // A freshly provisioned account owns exactly one team. With any other count we can't\n // tell which team the create call made, and guessing would capture into the wrong one.\n if (!teams.success || teams.data.length !== 1) {\n logToFile(\n '[provisioning] no unambiguous team to read the resource back from',\n );\n return null;\n }\n\n const resourceId = String(teams.data[0].id);\n\n try {\n const res = await axios.get(\n `${provisioningBaseUrl}/api/agentic/provisioning/resources/${resourceId}`,\n {\n headers: {\n Authorization: `Bearer ${accessToken}`,\n 'API-Version': API_VERSION,\n 'User-Agent': WIZARD_USER_AGENT,\n },\n timeout: 30_000,\n },\n );\n\n logToFile(\n `[provisioning] read resource ${resourceId} back after unreadable create`,\n );\n return readResource(res.data, 'resource_readback');\n } catch {\n logToFile(`[provisioning] read-back of resource ${resourceId} failed`);\n return null;\n }\n}\n\nexport interface ProvisioningResult {\n accessToken: string;\n refreshToken: string;\n /** Epoch ms when `accessToken` expires. */\n expiresAt: number;\n /** OAuth client the grant was minted under — refreshes must present the same one. */\n oauthClientId: string;\n projectApiKey: string;\n host: string;\n personalApiKey?: string;\n projectId: string;\n accountId: string;\n}\n\n/**\n * Create a new PostHog account and provision a project via the provisioning API.\n *\n * This is the \"no browser\" signup path: the wizard collects the email,\n * calls the provisioning API to create the account, and gets back\n * credentials without opening a browser.\n */\nexport async function provisionNewAccount(\n email: string,\n name: string,\n region: 'US' | 'EU' = 'US',\n opts?: {\n orgName?: string;\n projectName?: string;\n baseUrl?: string;\n /** Scope list to request; defaults to `WIZARD_PROVISIONING_SCOPES`. */\n scopes?: readonly string[];\n },\n): Promise<ProvisioningResult> {\n const codeVerifier = generateCodeVerifier();\n const codeChallenge = generateCodeChallenge(codeVerifier);\n const provisioningBaseUrl = getProvisioningBaseUrl(region, opts?.baseUrl);\n const oauthClientId = getProvisioningClientId(region, opts?.baseUrl);\n\n logToFile('[provisioning] starting account creation');\n\n // Step 1: Create account\n const accountRes = await axios.post(\n `${provisioningBaseUrl}/api/agentic/provisioning/account_requests`,\n {\n id: crypto.randomUUID(),\n email,\n name,\n client_id: oauthClientId,\n code_challenge: codeChallenge,\n code_challenge_method: 'S256',\n scopes: opts?.scopes ?? WIZARD_PROVISIONING_SCOPES,\n configuration: {\n region,\n ...(opts?.orgName ? { organization_name: opts.orgName } : {}),\n },\n },\n {\n headers: {\n 'Content-Type': 'application/json',\n 'API-Version': API_VERSION,\n 'User-Agent': WIZARD_USER_AGENT,\n },\n timeout: 30_000,\n },\n );\n\n const accountData = parseOrThrow(\n AccountRequestResponseSchema,\n accountRes.data,\n 'account_requests',\n );\n\n if (accountData.type === 'error') {\n const msg = accountData.error?.message ?? 'Account creation failed';\n analytics.captureException(new Error(msg), {\n step: 'provisioning_account_request',\n error_code: accountData.error?.code,\n });\n throw new Error(msg);\n }\n\n if (accountData.type === 'requires_auth') {\n throw new Error(\n 'This email is already associated with a PostHog account. Please use the login flow instead.',\n );\n }\n\n const code = accountData.oauth?.code;\n if (!code) {\n throw new Error('No authorization code received from account creation');\n }\n\n logToFile('[provisioning] account created, exchanging code for tokens');\n\n // Step 2: Exchange code for tokens\n const tokenRes = await axios.post(\n `${provisioningBaseUrl}/api/agentic/oauth/token`,\n new URLSearchParams({\n grant_type: 'authorization_code',\n code,\n code_verifier: codeVerifier,\n }).toString(),\n {\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded',\n 'API-Version': API_VERSION,\n 'User-Agent': WIZARD_USER_AGENT,\n },\n timeout: 30_000,\n },\n );\n\n const tokenData = parseOrThrow(\n TokenResponseSchema,\n tokenRes.data,\n 'oauth_token',\n );\n\n logToFile('[provisioning] tokens received, provisioning resources');\n\n // Step 3: Provision resources\n const resourceRes = await axios.post(\n `${provisioningBaseUrl}/api/agentic/provisioning/resources`,\n {\n ...(opts?.projectName\n ? { configuration: { project_name: opts.projectName } }\n : {}),\n },\n {\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${tokenData.access_token}`,\n 'API-Version': API_VERSION,\n 'User-Agent': WIZARD_USER_AGENT,\n },\n timeout: 30_000,\n },\n );\n\n // `/resources` is synchronous: it answers `status: \"complete\"` or an error envelope.\n // So an unreadable body means the contract moved, not that provisioning is still\n // running — and the project already exists either way, which is why this recovers\n // instead of aborting.\n let resource = readResource(resourceRes.data, 'resources');\n\n if (!resource.ok && resource.reason === 'unreadable') {\n resource =\n (await readBackResource(\n provisioningBaseUrl,\n tokenData.access_token,\n tokenRes.data,\n )) ?? resource;\n }\n\n if (!resource.ok) {\n if (resource.reason === 'incomplete') {\n throw new Error('Resource provisioning did not complete');\n }\n throw new ProvisionedAccountUnreadableError(\n 'Your PostHog project was created, but the provisioning API returned a response the wizard could not read',\n );\n }\n\n logToFile('[provisioning] resources provisioned successfully');\n\n return {\n accessToken: tokenData.access_token,\n refreshToken: tokenData.refresh_token,\n expiresAt: Date.now() + tokenData.expires_in * 1000,\n oauthClientId,\n projectApiKey: resource.access.api_key,\n host: resource.access.host,\n personalApiKey: resource.access.personal_api_key,\n projectId: resource.projectId,\n accountId: tokenData.account?.id ?? '',\n };\n}\n\n/**\n * Request a one-time deep link URL that logs the user into PostHog\n * and redirects to their project dashboard — or, when `opts.path` is\n * given, to any safe in-app relative path (e.g. the Signals inbox).\n */\nexport async function requestDeepLink(\n accessToken: string,\n host: HostResolution,\n opts?: { purpose?: string; path?: string },\n): Promise<string | null> {\n try {\n const baseUrl = host.appHost;\n\n const res = await axios.post(\n `${baseUrl}/api/agentic/provisioning/deep_links`,\n {\n purpose: opts?.purpose ?? 'dashboard',\n ...(opts?.path ? { path: opts.path } : {}),\n },\n {\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${accessToken}`,\n 'API-Version': API_VERSION,\n 'User-Agent': WIZARD_USER_AGENT,\n },\n timeout: 10_000,\n },\n );\n\n const url = res.data?.url;\n if (typeof url === 'string') {\n logToFile(`[provisioning] deep link created: ${url}`);\n return url;\n }\n return null;\n } catch {\n logToFile('[provisioning] deep link request failed, skipping');\n return null;\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { n as analytics } from "./analytics-
|
|
2
|
-
import { r as isValidModel, t as VALID_MODELS } from "./models-
|
|
3
|
-
import { r as redactSecrets } from "./results-
|
|
1
|
+
import { n as analytics } from "./analytics-J1cjIekQ.js";
|
|
2
|
+
import { r as isValidModel, t as VALID_MODELS } from "./models-HNKiMsME.js";
|
|
3
|
+
import { r as redactSecrets } from "./results-BE1ctlKP.js";
|
|
4
4
|
import * as fs$1 from "fs";
|
|
5
5
|
import * as path$1 from "path";
|
|
6
6
|
import { randomUUID } from "crypto";
|
|
@@ -621,4 +621,4 @@ function buildOrchestratorTools(tool, ctx) {
|
|
|
621
621
|
//#endregion
|
|
622
622
|
export { TaskStatus as _, REMARK_ASK as a, applyReadHandoffs as c, taskReported as d, uncoveredBySink as f, SkipReason as g, QueueStore as h, NOT_NEEDED_REASON_ASK as i, buildOrchestratorTools as l, QUEUE_DIR_NAME as m, ENQUEUE_MODEL_DESCRIPTION as n, applyComplete as o, NotNeededReason as p, HANDOFF_FIELDS as r, applyEnqueue as s, COMPLETE_TASK_DESCRIPTION as t, dependencyClosure as u, makeMutex as v, writeJsonAtomic as y };
|
|
623
623
|
|
|
624
|
-
//# sourceMappingURL=queue-tools-
|
|
624
|
+
//# sourceMappingURL=queue-tools-BS8nXJ6S.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"queue-tools-PFG6H93Q.js","names":["path"],"sources":["../src/shared/utils/atomic-ledger.ts","../src/agent/runner/sequence/orchestrator/queue.ts","../src/agent/runner/sequence/orchestrator/queue-tools.ts"],"sourcesContent":["/**\n * Small shared primitives for on-disk ledgers: an atomic JSON writer and a\n * single-chain async mutex. Used by the audit tools and by the orchestrator\n * queue. Lifted here so both share one implementation.\n */\nimport * as fs from 'fs';\n\n/**\n * Atomically write JSON: write to a `.tmp` file then rename over the target. The\n * rename bumps the file's mtime in one step, which is what a file watcher polls.\n */\nexport function writeJsonAtomic(targetPath: string, data: unknown): void {\n const tmpPath = `${targetPath}.tmp`;\n fs.writeFileSync(tmpPath, JSON.stringify(data, null, 2), 'utf8');\n fs.renameSync(tmpPath, targetPath);\n}\n\n/**\n * A single async mutex. Serializes read-modify-write cycles so concurrent callers\n * (parallel task agents, audit tool calls) never interleave a mutation.\n */\nexport function makeMutex() {\n let chain: Promise<unknown> = Promise.resolve();\n return async function run<T>(fn: () => Promise<T> | T): Promise<T> {\n const next = chain.then(() => fn());\n chain = next.catch(() => undefined);\n return next;\n };\n}\n","/**\n * The orchestrator task queue.\n *\n * In memory, synchronous, single-owner: one Node process drives the run, so\n * there is no locking. The queue imposes no execution policy — `nextRunnable`\n * returns every pending task whose dependencies are satisfied, and how many of\n * those run at once is decided by the task graph, not the queue.\n *\n * Every transition rewrites `<installDir>/.posthog-wizard-cache/queue.json`, a\n * small file holding the whole queue, handoffs included. It is the run's log\n * and the report's source. The whole cache folder is run-scoped and wiped when\n * the run ends.\n */\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { randomUUID } from 'crypto';\nimport { writeJsonAtomic } from '@utils/atomic-ledger';\nimport { analytics } from '@utils/analytics';\n\nexport const TaskStatus = {\n Pending: 'pending',\n Running: 'running',\n Done: 'done',\n Skipped: 'not needed',\n Failed: 'failed',\n} as const;\n\nexport type TaskStatus = (typeof TaskStatus)[keyof typeof TaskStatus];\n\n/**\n * Why a task ended as skipped.\n *\n * A skip has several causes that look identical from outside: an agent finding\n * the step does not apply, a user declining it, and an offer nobody answered.\n * They mean opposite things — the first is a no-op, the last two are the user\n * telling us something — and telling them apart needs the reason recorded at\n * the moment of the skip, not inferred later from timings.\n *\n * Recorded on the task like `error` is on a failed one, so it reaches the\n * skipped-task event and the run's queue.json alike.\n */\nexport const SkipReason = {\n /** The user answered the step's notice with Skip. */\n UserDeclined: 'user-declined',\n /** The notice went unanswered until it timed out. */\n NoticeTimeout: 'notice-timeout',\n /** The notice could not be shown at all, so consent failed closed. */\n NoticeError: 'notice-error',\n /** The task agent reported `not needed`. See {@link NotNeededReason} for why. */\n AgentNotNeeded: 'agent-not-needed',\n} as const;\n\nexport type SkipReason = (typeof SkipReason)[keyof typeof SkipReason];\n\n/**\n * Why the agent itself reported `not needed`.\n *\n * {@link SkipReason.AgentNotNeeded} records who decided the skip, not what they\n * decided. `complete_task` offers `not needed` both for \"the step does not\n * apply\" and for \"you cannot do it\", so an agent that never got a credential\n * out of the user lands in the same bucket as one that found nothing to\n * connect — and the bucket is named after the first meaning. For the one step\n * that stops to ask for credentials, that is the difference that matters: a\n * step nobody supplied a password for is not a step that did not apply.\n *\n * A sub-dimension of the skip reason, never a replacement for it, so every\n * existing reason value keeps counting exactly what it counted before.\n */\nexport const NotNeededReason = {\n /** Nothing to do: the step genuinely does not apply to this project. */\n NotApplicable: 'not-applicable',\n /** The user was asked and declined, or left the prompts unanswered. */\n UserDeclined: 'user-declined',\n /** Something outside the run stopped it — a credential, plan, or endpoint. */\n Blocked: 'blocked',\n} as const;\n\nexport type NotNeededReason =\n (typeof NotNeededReason)[keyof typeof NotNeededReason];\n\n/**\n * Whether a value names a reason. The pi harness hands tool arguments over\n * unvalidated, so an invented value would otherwise reach the analytics\n * dimension as free text — the one thing this step must never emit.\n */\nexport function isNotNeededReason(value: unknown): value is NotNeededReason {\n return (Object.values(NotNeededReason) as unknown[]).includes(value);\n}\n\nexport interface QueuedTask {\n id: string;\n type: string;\n /** Human-readable label for the TUI, set by the enqueuing agent. */\n label?: string;\n status: TaskStatus;\n /**\n * Ids of tasks that must finish before this one runs. Ids are generated at\n * enqueue, so a task can only depend on tasks created before it — the graph is\n * a DAG by construction, cycles cannot form. Unknown ids are rejected by the\n * enqueue_task guard.\n *\n * One sanctioned exception: {@link QueueStore.addDependencies} adds edges to a\n * still-pending task, for a runner-seeded task that was queued before the\n * planner ran and so could not name its dependencies then. It is additive,\n * pending-only, and cycle-checked per edge, so the DAG property above still\n * holds. Nothing else mutates this field.\n */\n dependsOn: string[];\n inputs: Record<string, unknown>;\n model?: string;\n attempts: number;\n maxAttempts: number;\n /** The structured handoff the task reported on completion. */\n handoff?: TaskHandoff;\n /** 'orchestrator' for seeded tasks, or the id of the task that enqueued this one. */\n enqueuedBy: string;\n /** Terminal failure unblocks dependents and never fails the run. */\n optional?: boolean;\n createdAt: string;\n startedAt?: string;\n finishedAt?: string;\n error?: { type: string; message: string };\n /** Set when `status` is `not needed`: why. The failed-task `error` of a skip. */\n skipReason?: SkipReason;\n /** Set only for an agent-reported skip, and only when the agent declared it. */\n notNeededReason?: NotNeededReason;\n}\n\nexport interface QueueFile {\n version: 1;\n runId: string;\n tasks: QueuedTask[];\n}\n\nexport interface TaskOutcome {\n type: string;\n status: TaskStatus;\n optional: boolean;\n}\n\n/** The structured handoff a task leaves for the next agent. */\nexport interface TaskHandoff {\n goals: string;\n did: string;\n forNextAgent: string;\n filesTouched?: string[];\n /** How the agent knows it worked — what it ran or observed. */\n evidence?: string;\n /** What the agent assumed about the app and could not verify. */\n assumptions?: string;\n /** A one-line summary of any unresolved conflict, surfaced in the outro. */\n conflict?: string;\n /** A finished section for the run's report, written by the task that owns the subject. */\n reportSection?: string;\n}\n\nexport interface EnqueueInput {\n type: string;\n label?: string;\n inputs?: Record<string, unknown>;\n dependsOn?: string[];\n model?: string;\n maxAttempts?: number;\n enqueuedBy?: string;\n optional?: boolean;\n}\n\nexport const QUEUE_DIR_NAME = '.posthog-wizard-cache';\nconst DEFAULT_MAX_ATTEMPTS = 2;\n\nfunction nowIso(): string {\n return new Date().toISOString();\n}\n\n/** Dropped in the cache folder so an orphaned copy explains itself. */\nconst DELETE_ME_FILE = '.DELETE-ME.md';\nconst DELETE_ME_BODY = `# Safe to delete\n\nThis folder contains run artifacts from the PostHog Wizard. This should have\nbeen deleted if the Wizard has finished running. If this wasn't deleted for\nsome reason, you can safely delete the entire \\`${QUEUE_DIR_NAME}/\\` folder.\n`;\n\n/** Every queue transition, in the order it is reflected. */\nexport type TransitionEvent =\n | 'enqueue'\n | 'start'\n | 'complete'\n | 'skip'\n | 'fail'\n | 'requeue';\n\nexport interface QueueStoreOptions {\n /**\n * Called on every transition with the task's post-transition state. The\n * runner uses it for telemetry; the store itself stays analytics-free.\n * Listener errors are reported but cannot break a transition.\n */\n onTransition?: (event: TransitionEvent, task: QueuedTask) => void;\n}\n\nexport class QueueStore {\n private tasks: QueuedTask[] = [];\n private readonly onTransition?: (\n event: TransitionEvent,\n task: QueuedTask,\n ) => void;\n\n readonly runId: string;\n readonly queuePath: string;\n\n constructor(installDir: string, runId: string, opts?: QueueStoreOptions) {\n this.onTransition = opts?.onTransition;\n this.runId = runId;\n const dir = path.join(installDir, QUEUE_DIR_NAME);\n this.queuePath = path.join(dir, 'queue.json');\n fs.mkdirSync(dir, { recursive: true });\n fs.writeFileSync(path.join(dir, DELETE_ME_FILE), DELETE_ME_BODY);\n }\n\n // ── Reads ───────────────────────────────────────────────────────────\n\n list(): readonly QueuedTask[] {\n return this.tasks;\n }\n\n get(id: string): QueuedTask | undefined {\n return this.tasks.find((t) => t.id === id);\n }\n\n /**\n * Every pending task whose dependencies are all satisfied (`done` or\n * `skipped`). A skipped dependency does not block downstream work.\n */\n nextRunnable(): QueuedTask[] {\n // A TERMINALLY failed optional dep satisfies like skipped; retryable failure still blocks.\n const doneIds = new Set(\n this.tasks\n .filter(\n (t) =>\n t.status === TaskStatus.Done ||\n t.status === TaskStatus.Skipped ||\n (t.status === TaskStatus.Failed &&\n t.optional === true &&\n t.attempts >= t.maxAttempts),\n )\n .map((t) => t.id),\n );\n return this.tasks.filter(\n (t) =>\n t.status === TaskStatus.Pending &&\n t.dependsOn.every((d) => doneIds.has(d)),\n );\n }\n\n /**\n * True when no task is running and none can be started. Either everything\n * is terminal, or the only pending tasks are blocked by a failed dependency.\n */\n isDrained(): boolean {\n if (this.tasks.some((t) => t.status === TaskStatus.Running)) return false;\n return this.nextRunnable().length === 0;\n }\n\n summary(): Record<TaskStatus, number> & { total: number } {\n const counts: Record<TaskStatus, number> = {\n [TaskStatus.Pending]: 0,\n [TaskStatus.Running]: 0,\n [TaskStatus.Done]: 0,\n [TaskStatus.Skipped]: 0,\n [TaskStatus.Failed]: 0,\n };\n for (const t of this.tasks) counts[t.status] += 1;\n return { ...counts, total: this.tasks.length };\n }\n\n readHandoff(id: string): TaskHandoff | null {\n return this.get(id)?.handoff ?? null;\n }\n\n /** Handoffs of completed tasks of a given type, oldest first. */\n readHandoffsByType(type: string): TaskHandoff[] {\n return this.tasks\n .filter((t) => t.type === type && t.handoff)\n .map((t) => t.handoff as TaskHandoff);\n }\n\n // ── Transitions (each one reflected to queue.json) ──────────────────\n\n enqueue(input: EnqueueInput): QueuedTask {\n const task: QueuedTask = {\n id: randomUUID(),\n type: input.type,\n label: input.label,\n status: TaskStatus.Pending,\n dependsOn: input.dependsOn ?? [],\n inputs: input.inputs ?? {},\n model: input.model,\n attempts: 0,\n maxAttempts: input.maxAttempts ?? DEFAULT_MAX_ATTEMPTS,\n enqueuedBy: input.enqueuedBy ?? 'orchestrator',\n optional: input.optional,\n createdAt: nowIso(),\n };\n this.tasks.push(task);\n this.reflect();\n this.notify('enqueue', task);\n return task;\n }\n\n /**\n * Add dependency edges to a pending task — the one sanctioned exception to\n * `dependsOn` being immutable (see the field's doc).\n *\n * A runner-seeded task is enqueued before the planner runs, so the tasks it\n * should wait for do not exist yet and it cannot name them. This closes that\n * gap once, between planning and the drain.\n *\n * Three rules keep the graph a DAG: additive only (never removes an edge),\n * pending only (a task that already started keeps the graph it ran under), and\n * every edge cycle-checked — a dep that already reaches this task through its\n * own chain is refused. Refusals come back in the result rather than throwing:\n * one bad edge is a bug worth reporting, not a reason to end a run that is\n * otherwise fine.\n */\n addDependencies(\n id: string,\n depIds: readonly string[],\n ): { added: string[]; refused: string[] } {\n const task = this.require(id);\n if (task.status !== TaskStatus.Pending) {\n return { added: [], refused: [...depIds] };\n }\n\n const added: string[] = [];\n const refused: string[] = [];\n for (const depId of depIds) {\n // Already an edge, or a self-loop: nothing to do and nothing wrong.\n if (depId === id || task.dependsOn.includes(depId)) continue;\n // Checked against the graph as it stands, so two edges added in one call\n // cannot together form a cycle the first check missed.\n if (!this.get(depId) || this.reaches(depId, id)) {\n refused.push(depId);\n continue;\n }\n task.dependsOn.push(depId);\n added.push(depId);\n }\n if (added.length > 0) this.reflect();\n return { added, refused };\n }\n\n start(id: string): QueuedTask {\n const t = this.require(id);\n t.status = TaskStatus.Running;\n t.startedAt = nowIso();\n t.attempts += 1;\n this.reflect();\n this.notify('start', t);\n return t;\n }\n\n complete(id: string, handoff?: TaskHandoff): QueuedTask {\n return this.finish(id, TaskStatus.Done, handoff);\n }\n\n /**\n * Terminal: the task was not done, and that is not a failure.\n *\n * The reason is required, and sits before the optional handoff for that\n * reason. A skip carrying no reason is what let a five-minute auto-decline\n * hide inside the same event as an agent deciding a step did not apply.\n *\n * `notNeededReason` splits the agent-reported reason one level further; only\n * an agent supplies it, and only when it declared one.\n */\n skip(\n id: string,\n reason: SkipReason,\n handoff?: TaskHandoff,\n notNeededReason?: NotNeededReason,\n ): QueuedTask {\n const t = this.require(id);\n t.skipReason = reason;\n if (notNeededReason) t.notNeededReason = notNeededReason;\n return this.finish(id, TaskStatus.Skipped, handoff);\n }\n\n fail(\n id: string,\n error: { type: string; message: string },\n handoff?: TaskHandoff,\n ): QueuedTask {\n const t = this.require(id);\n t.error = error;\n return this.finish(id, TaskStatus.Failed, handoff);\n }\n\n /** Put a failed/running task back to pending for a retry within the run. */\n requeue(id: string): QueuedTask {\n const t = this.require(id);\n t.status = TaskStatus.Pending;\n t.startedAt = undefined;\n t.finishedAt = undefined;\n this.reflect();\n this.notify('requeue', t);\n return t;\n }\n\n // ── Internals ───────────────────────────────────────────────────────\n\n private finish(\n id: string,\n status: 'done' | 'not needed' | 'failed',\n handoff?: TaskHandoff,\n ): QueuedTask {\n const t = this.require(id);\n if (handoff) t.handoff = handoff;\n t.status = status;\n t.finishedAt = nowIso();\n this.reflect();\n this.notify(\n status === TaskStatus.Done\n ? 'complete'\n : status === TaskStatus.Skipped\n ? 'skip'\n : 'fail',\n t,\n );\n return t;\n }\n\n private reflect(): void {\n const file: QueueFile = {\n version: 1,\n runId: this.runId,\n tasks: this.tasks,\n };\n writeJsonAtomic(this.queuePath, file);\n }\n\n private notify(event: TransitionEvent, task: QueuedTask): void {\n try {\n this.onTransition?.(event, task);\n } catch (error) {\n // A listener must never break a transition, but its failure is a bug.\n analytics.captureException(\n error instanceof Error ? error : new Error(String(error)),\n { step: 'orchestrator_queue_listener', event },\n );\n }\n }\n\n /** Whether `fromId` reaches `targetId` by following dependsOn edges. */\n private reaches(fromId: string, targetId: string): boolean {\n const seen = new Set<string>();\n const stack = [fromId];\n while (stack.length > 0) {\n const current = stack.pop() as string;\n if (current === targetId) return true;\n if (seen.has(current)) continue;\n seen.add(current);\n stack.push(...(this.get(current)?.dependsOn ?? []));\n }\n return false;\n }\n\n private require(id: string): QueuedTask {\n const t = this.get(id);\n if (!t) throw new Error(`No task ${id} in the queue`);\n return t;\n }\n}\n","/**\n * Orchestrator MCP tools, registered into the existing `wizard-tools` server when\n * a queue is present. They let the orchestrator agent and task agents grow the\n * queue, report completion with a structured handoff, and read prior handoffs.\n *\n * The guard logic and the apply functions are plain, exported, and unit-tested.\n * `buildOrchestratorTools` wraps them in the SDK `tool()` shape.\n */\nimport { z } from 'zod';\nimport { analytics } from '@utils/analytics';\nimport { redactSecrets } from '@shared/mcp-clients/results';\nimport { isValidModel, VALID_MODELS } from '../../switchboard/models';\nimport {\n isNotNeededReason,\n NotNeededReason,\n SkipReason,\n TaskStatus,\n type QueueStore,\n type QueuedTask,\n type TaskHandoff,\n} from './queue';\n\n/**\n * The `complete_task` `notNeededReason` description, shared by both harnesses'\n * schemas so the ask cannot drift between them.\n *\n * Named per value rather than left to prose: the distinction the flow needs is\n * \"the user did not hand over a credential\" against \"there was nothing here to\n * connect\", and an agent asked for that in free text writes it into the handoff\n * — where {@link applyComplete} deliberately cannot forward it, because handoff\n * prose on this step reaches live database passwords.\n */\nexport const NOT_NEEDED_REASON_ASK = `Required when status is 'not needed': which of these ended the task. '${NotNeededReason.NotApplicable}' — the step genuinely does not apply to this project. '${NotNeededReason.UserDeclined}' — you asked the user and they declined, cancelled, or never answered. '${NotNeededReason.Blocked}' — something outside your and the user's control stopped you (a credential the project does not have, a plan or permission it lacks, an endpoint you could not reach). Ignored for any other status.`;\n\n/**\n * The `enqueue_task` `model` description, shared by both harnesses' schemas.\n *\n * The field was declared as a bare optional string while\n * {@link checkEnqueueGuards} rejects anything outside {@link VALID_MODELS}, so\n * the allow-list existed only in the rejection message — an agent had to guess\n * a gateway model id, trip the `invalid-model` guard, and spend a turn reading\n * the list back before it could enqueue. Naming the list where the agent picks\n * the value costs nothing and makes the guess unnecessary.\n */\nexport const ENQUEUE_MODEL_DESCRIPTION = `Optional model override for this task. Omit it to use the task's default, which is almost always right. If you do set it, it must be one of: ${[\n ...VALID_MODELS,\n].join(', ')}.`;\n\n/**\n * The `complete_task` tool description, shared by both harnesses' schemas so\n * the outcome contract cannot drift — the same discipline {@link HANDOFF_FIELDS}\n * applies to the fields inside the handoff.\n *\n * The closing sentence names the nesting because agents kept learning it from a\n * rejection instead: a first call with `goals`/`did`/`forNextAgent` at the top\n * level fails schema validation, and reading that error back was the only place\n * the shape was stated. Naming it where the agent fills the call in costs\n * nothing, the same reasoning as {@link ENQUEUE_MODEL_DESCRIPTION}.\n */\nexport const COMPLETE_TASK_DESCRIPTION =\n 'Report the outcome of your task. Always call this exactly once when you finish, with a structured handoff for the next agent. ' +\n \"Use status 'not needed' when the task does not apply to this project and you cannot do it (say why in the handoff) — not 'done'. \" +\n 'The handoff is one nested object: put `goals`, `did` and `forNextAgent` — all three required — inside `handoff`, along with any optional fields, never at the top level.';\n\n/** The per-task remark ask, shared by both harnesses' complete_task schemas. */\nexport const REMARK_ASK =\n 'What information or guidance would have been useful to have in the integration prompt or documentation for this task — specifically anything that would have prevented tool failures, erroneous edits, or other wasted turns.';\n\nexport interface OrchestratorToolsContext {\n store: QueueStore;\n /** Task types the registry knows about. enqueue_task rejects anything else. */\n validTypes: readonly string[];\n /**\n * Types marked `sink: true` — the ones that run last and must therefore\n * depend, transitively, on every other task in the queue. Enqueuing one with\n * an incomplete closure is rejected, so a task the runner seeded before the\n * planner ran can never be left un-awaited.\n */\n sinkTypes?: readonly string[];\n /**\n * Types the wizard queues itself, before the planner runs. They are deferred\n * to the end of the drain and may stop to ask the user for input, so the edge\n * to them is one-way: the sink depends on them, nothing else may. See\n * {@link seededDepViolations}.\n */\n runnerSeededTypes?: readonly string[];\n /**\n * Types marked `optional: true` in their frontmatter. Enqueue stamps the flag\n * from this list — the task's definition decides, never the enqueuing agent —\n * so terminal failure of such a task unblocks dependents and never fails the\n * run.\n */\n optionalTypes?: readonly string[];\n /**\n * The id of the task this tool server is bound to. Each task agent gets its\n * own wizard-tools server, so attribution holds when independent tasks run\n * in parallel. Absent for the seed, which is not a task.\n */\n currentTaskId?: string;\n}\n\n/**\n * Whether the task this context is bound to has reported through\n * `complete_task`. False for the seed, which is not a task.\n */\nexport function taskReported(ctx: OrchestratorToolsContext): boolean {\n if (!ctx.currentTaskId) return false;\n const status = ctx.store.get(ctx.currentTaskId)?.status;\n return (\n status === TaskStatus.Done ||\n status === TaskStatus.Failed ||\n status === TaskStatus.Skipped\n );\n}\n\nexport interface EnqueueArgs {\n type: string;\n label?: string;\n inputs?: Record<string, unknown>;\n dependsOn?: string[];\n model?: string;\n reason: string;\n}\n\nexport type GuardResult =\n | { ok: true }\n | { ok: false; guard: string; message: string };\n\nfunction stableStringify(value: unknown): string {\n if (value === null || typeof value !== 'object') return JSON.stringify(value);\n if (Array.isArray(value)) return `[${value.map(stableStringify).join(',')}]`;\n const entries = Object.entries(value as Record<string, unknown>).sort(\n ([a], [b]) => a.localeCompare(b),\n );\n return `{${entries\n .map(([k, v]) => `${JSON.stringify(k)}:${stableStringify(v)}`)\n .join(',')}}`;\n}\n\nfunction dedupKey(type: string, inputs: Record<string, unknown>): string {\n return `${type}::${stableStringify(inputs)}`;\n}\n\n/**\n * A backstop on total queue size. Tasks can enqueue tasks, so a misbehaving\n * type could grow the queue without bound. Keeping the graph small is the job\n * of good agent and skill design, not this number — it only stops a runaway.\n * The real flow is ~9 tasks, so this sits well clear of it.\n */\nconst MAX_QUEUE_TASKS = 30;\n\n/** Every task id reachable from `roots` through dependsOn edges, roots included. */\nexport function dependencyClosure(\n store: QueueStore,\n roots: readonly string[],\n): Set<string> {\n const seen = new Set<string>();\n const visit = (id: string): void => {\n if (seen.has(id)) return;\n seen.add(id);\n for (const dep of store.get(id)?.dependsOn ?? []) visit(dep);\n };\n for (const id of roots) visit(id);\n return seen;\n}\n\n/**\n * Tasks a sink would fail to wait for. A sink runs last by definition, so any\n * queued task outside its dependency closure is work whose handoff the sink\n * would miss — the exact failure mode of a task seeded before the planner ran.\n * Empty for a non-sink type, and for a sink that covers everything.\n */\nexport function uncoveredBySink(\n ctx: OrchestratorToolsContext,\n args: Pick<EnqueueArgs, 'type' | 'dependsOn'>,\n): QueuedTask[] {\n if (!ctx.sinkTypes?.includes(args.type)) return [];\n const covered = dependencyClosure(ctx.store, args.dependsOn ?? []);\n return ctx.store.list().filter((t) => !covered.has(t.id));\n}\n\n/**\n * Runner-seeded tasks a non-sink task would end up waiting for.\n *\n * The edge to a runner-seeded task is one-way. Such a task is deferred to the\n * end of the drain and may stop to ask the user for input, so anything that\n * waits on it inherits that wait — a coding step gated on someone typing a\n * database password. The sink is the sole exception: it runs last by\n * definition and must depend on every task, this one included.\n *\n * The whole closure is checked, not just direct dependencies, so the rule\n * cannot be reached through an intermediate hop.\n *\n * Empty for a sink, and for any task that does not reach a seeded task.\n */\nexport function seededDepViolations(\n ctx: OrchestratorToolsContext,\n args: Pick<EnqueueArgs, 'type' | 'dependsOn'>,\n): QueuedTask[] {\n const seeded = ctx.runnerSeededTypes ?? [];\n if (seeded.length === 0) return [];\n if (ctx.sinkTypes?.includes(args.type)) return [];\n const reached = dependencyClosure(ctx.store, args.dependsOn ?? []);\n return ctx.store\n .list()\n .filter((t) => reached.has(t.id) && seeded.includes(t.type));\n}\n\n/**\n * Validate an enqueue. Structural checks only — a real type, real dependencies,\n * a sink that waits for everything, no non-sink task waiting on a runner-seeded\n * one, not a literal duplicate, and not past the runaway backstop. How much\n * runs, and in what shape, is the task graph's business, not a knob's.\n */\nexport function checkEnqueueGuards(\n ctx: OrchestratorToolsContext,\n args: EnqueueArgs,\n): GuardResult {\n const tasks = ctx.store.list();\n\n if (tasks.length >= MAX_QUEUE_TASKS) {\n return {\n ok: false,\n guard: 'queue-full',\n message: `The queue already holds ${tasks.length} tasks (cap ${MAX_QUEUE_TASKS}). Refine the existing tasks rather than adding more.`,\n };\n }\n\n if (!ctx.validTypes.includes(args.type)) {\n return {\n ok: false,\n guard: 'unknown-type',\n message: `Unknown task type \"${\n args.type\n }\". Valid types: ${ctx.validTypes.join(', ')}.`,\n };\n }\n\n // Reject an agent pinning a task to a non-allow-listed model.\n if (args.model !== undefined && !isValidModel(args.model)) {\n return {\n ok: false,\n guard: 'invalid-model',\n message: `Model \"${\n args.model\n }\" is not allowed. Omit model to use the task default, or pick one of: ${[\n ...VALID_MODELS,\n ].join(', ')}.`,\n };\n }\n\n for (const dep of args.dependsOn ?? []) {\n if (!ctx.store.get(dep)) {\n return {\n ok: false,\n guard: 'unknown-dep',\n message: `Dependency \"${dep}\" is not a known task id.`,\n };\n }\n }\n\n const seededDeps = seededDepViolations(ctx, args);\n if (seededDeps.length > 0) {\n return {\n ok: false,\n guard: 'seeded-dep',\n message: `A \"${args.type}\" task must not wait for ${seededDeps\n .map((t) => `${t.type} (${t.id})`)\n .join(\n ', ',\n )} — directly or through its dependencies. The wizard placed those, runs them at the end, and they stop to ask the user for input, so anything waiting on one waits on a person. Remove them from dependsOn and enqueue it again; only the final reporting task may depend on them.`,\n };\n }\n\n const uncovered = uncoveredBySink(ctx, args);\n if (uncovered.length > 0) {\n const seededUncovered = uncovered.filter((t) =>\n (ctx.runnerSeededTypes ?? []).includes(t.type),\n );\n // \"Or to a task already in dependsOn\" is the right advice for an ordinary\n // task and exactly wrong for a runner-seeded one — routing it through an\n // intermediate satisfies the closure while breaking the one-way rule above.\n // So a seeded task is named with the only placement that is legal for it.\n const how =\n seededUncovered.length === uncovered.length\n ? \"Add them to this task's own dependsOn — for a task the wizard placed, that is the only legal spot — and enqueue it again.\"\n : 'Add them to dependsOn, or to a task already in dependsOn, and enqueue it again.';\n return {\n ok: false,\n guard: 'sink-closure',\n message: `A \"${\n args.type\n }\" task runs last, so it must depend on every other task — directly or through its dependencies. These are not covered: ${uncovered\n .map((t) => `${t.type} (${t.id})`)\n .join(', ')}. ${how}`,\n };\n }\n\n const key = dedupKey(args.type, args.inputs ?? {});\n if (\n tasks.some(\n (t) =>\n t.status !== TaskStatus.Failed && dedupKey(t.type, t.inputs) === key,\n )\n ) {\n return {\n ok: false,\n guard: 'dedup',\n message: `A \"${args.type}\" task with these inputs already exists.`,\n };\n }\n\n return { ok: true };\n}\n\nexport type EnqueueResult =\n | { ok: true; task: QueuedTask }\n | { ok: false; guard: string; message: string };\n\nexport function applyEnqueue(\n ctx: OrchestratorToolsContext,\n args: EnqueueArgs,\n): EnqueueResult {\n const guard = checkEnqueueGuards(ctx, args);\n if (!guard.ok) return guard;\n\n const task = ctx.store.enqueue({\n type: args.type,\n label: args.label,\n inputs: args.inputs ?? {},\n dependsOn: args.dependsOn ?? [],\n model: args.model,\n optional: (ctx.optionalTypes ?? []).includes(args.type) || undefined,\n enqueuedBy: ctx.currentTaskId ?? 'orchestrator',\n });\n return { ok: true, task };\n}\n\nexport type CompleteResult = { ok: true } | { ok: false; message: string };\n\nexport type CompleteArgs = {\n status: 'done' | 'failed' | 'not needed';\n handoff: TaskHandoff;\n remark?: string;\n notNeededReason?: NotNeededReason;\n};\n\nexport function applyComplete(\n ctx: OrchestratorToolsContext,\n args: CompleteArgs,\n): CompleteResult {\n const id = ctx.currentTaskId;\n if (!id) {\n return {\n ok: false,\n message: 'complete_task can only be called by a running task agent.',\n };\n }\n if (args.remark) {\n analytics.wizardCapture('orchestrator remark', {\n task_type: ctx.store.get(id)?.type,\n remark: redactSecrets(args.remark),\n });\n }\n if (args.status === TaskStatus.Failed) {\n ctx.store.fail(\n id,\n { type: 'self-reported', message: args.handoff.forNextAgent },\n args.handoff,\n );\n } else if (args.status === TaskStatus.Skipped) {\n // The agent's own words stay in the handoff and out of telemetry. This flow\n // reaches live database and API credentials, and the repo has no redaction\n // pass for handoff prose, so the event carries the task type, the reason,\n // and the closed set of `notNeededReason` values — enough to separate an\n // agent no-op from a user decline from a blocked step, with no free text.\n ctx.store.skip(\n id,\n SkipReason.AgentNotNeeded,\n args.handoff,\n isNotNeededReason(args.notNeededReason)\n ? args.notNeededReason\n : undefined,\n );\n } else {\n ctx.store.complete(id, args.handoff);\n }\n return { ok: true };\n}\n\nexport function applyReadHandoffs(\n ctx: OrchestratorToolsContext,\n args: { type?: string; taskId?: string },\n): TaskHandoff[] {\n if (args.taskId) {\n const h = ctx.store.readHandoff(args.taskId);\n return h ? [h] : [];\n }\n if (args.type) {\n return ctx.store.readHandoffsByType(args.type);\n }\n // No filter: every handoff of a dependency of the current task.\n const currentId = ctx.currentTaskId;\n const current = currentId ? ctx.store.get(currentId) : undefined;\n if (!current) return [];\n return current.dependsOn\n .map((depId) => ctx.store.readHandoff(depId))\n .filter((h): h is TaskHandoff => h !== null);\n}\n\n// Caps each LLM-authored free-text field; queue.json rewrites whole per transition.\nconst HANDOFF_TEXT_MAX = 8_000;\n\n/**\n * The one description of every handoff field, shared by both harnesses'\n * `complete_task` schemas — the zod shape below and the typebox mirror in\n * `harness/pi/orchestrator-tools.ts`. A field an agent cannot see is a field it\n * cannot fill, so the two drifting silently drops whatever the missing field\n * carried; `__tests__/handoff-schema-parity.test.ts` holds them level.\n */\nexport const HANDOFF_FIELDS = {\n goals: 'What this task was asked to achieve.',\n did: 'What you actually did — for each file you edited: the change, the intention behind it, and the analytics it should feed (the insight, funnel, or dashboard tile it becomes part of).',\n forNextAgent: 'What the next agent should know.',\n filesTouched: 'Paths of every file you edited.',\n evidence:\n 'How you know it worked — what you ran or observed, not what you expect.',\n assumptions: 'What you assumed about the app and could not verify.',\n conflict:\n 'A one-line summary of any conflict you could not cleanly resolve (e.g. a dependency or build conflict). Put full detail in your work; this line is surfaced to the user.',\n reportSection:\n 'A finished markdown section about your work for the run report, written only when your task instructions ask for one. The reporting task includes it as its own section instead of rewriting it.',\n} as const satisfies Record<keyof Required<TaskHandoff>, string>;\n\nconst HANDOFF_SHAPE = {\n goals: z.string().max(HANDOFF_TEXT_MAX).describe(HANDOFF_FIELDS.goals),\n did: z.string().max(HANDOFF_TEXT_MAX).describe(HANDOFF_FIELDS.did),\n forNextAgent: z\n .string()\n .max(HANDOFF_TEXT_MAX)\n .describe(HANDOFF_FIELDS.forNextAgent),\n filesTouched: z\n .array(z.string().max(1_000))\n .max(200)\n .optional()\n .describe(HANDOFF_FIELDS.filesTouched),\n evidence: z\n .string()\n .max(HANDOFF_TEXT_MAX)\n .optional()\n .describe(HANDOFF_FIELDS.evidence),\n assumptions: z\n .string()\n .max(HANDOFF_TEXT_MAX)\n .optional()\n .describe(HANDOFF_FIELDS.assumptions),\n conflict: z\n .string()\n .max(HANDOFF_TEXT_MAX)\n .optional()\n .describe(HANDOFF_FIELDS.conflict),\n reportSection: z\n .string()\n .max(HANDOFF_TEXT_MAX)\n .optional()\n .describe(HANDOFF_FIELDS.reportSection),\n};\n\n/** Exported so the parity test can compare both harnesses' field sets. */\nexport const HANDOFF_SHAPE_KEYS: readonly string[] = Object.keys(HANDOFF_SHAPE);\n\n/**\n * `complete_task`'s own arguments, held level with the pi mirror by the same\n * parity test that guards the handoff — a top-level field can go missing on the\n * harness that runs just as easily as a nested one.\n */\nconst COMPLETE_SHAPE = {\n status: z.enum(['done', 'failed', 'not needed']),\n handoff: z.object(HANDOFF_SHAPE),\n remark: z.string().optional().describe(REMARK_ASK),\n notNeededReason: z\n .enum([\n NotNeededReason.NotApplicable,\n NotNeededReason.UserDeclined,\n NotNeededReason.Blocked,\n ])\n .optional()\n .describe(NOT_NEEDED_REASON_ASK),\n};\n\n/** Exported so the parity test can compare both harnesses' field sets. */\nexport const COMPLETE_SHAPE_KEYS: readonly string[] =\n Object.keys(COMPLETE_SHAPE);\n\ntype SdkTool = (\n name: string,\n description: string,\n // The SDK accepts a plain object of zod fields as the schema.\n schema: Record<string, z.ZodTypeAny>,\n handler: (args: never) => unknown,\n) => unknown;\n\nfunction textResult(text: string, isError = false) {\n return { isError, content: [{ type: 'text' as const, text }] };\n}\n\n/**\n * Build the orchestrator tools in the SDK `tool()` shape. Called from\n * createWizardToolsServer only when a queue context is present.\n */\nexport function buildOrchestratorTools(\n tool: SdkTool,\n ctx: OrchestratorToolsContext,\n): unknown[] {\n const enqueueTask = tool(\n 'enqueue_task',\n 'Add a task to the orchestrator queue. Use it to seed work and to enqueue follow-up work you discover. Keep tasks small and discrete.',\n {\n type: z\n .string()\n .describe(`The task type. One of: ${ctx.validTypes.join(', ')}.`),\n label: z\n .string()\n .optional()\n .describe(\n 'A short label for the UI — the action in a few words (e.g. \"Add the PostHog SDK\", \"Initialize PostHog\"). Leave out file names, class names, and other specifics.',\n ),\n inputs: z.record(z.unknown()).optional(),\n dependsOn: z\n .array(z.string())\n .optional()\n .describe('Task ids that must be done before this task runs.'),\n model: z.string().optional().describe(ENQUEUE_MODEL_DESCRIPTION),\n reason: z.string().describe('One line on why this task is needed.'),\n },\n ((args: EnqueueArgs) => {\n const res = applyEnqueue(ctx, args);\n if (!res.ok) {\n analytics.wizardCapture('orchestrator guard tripped', {\n guard: res.guard,\n type: args.type,\n });\n return textResult(res.message, true);\n }\n return textResult(JSON.stringify({ id: res.task.id }));\n }) as (args: never) => unknown,\n );\n\n const completeTask = tool(\n 'complete_task',\n COMPLETE_TASK_DESCRIPTION,\n COMPLETE_SHAPE,\n ((args: CompleteArgs) => {\n const res = applyComplete(ctx, args);\n if (!res.ok) return textResult(res.message, true);\n return textResult('ok');\n }) as (args: never) => unknown,\n );\n\n const readHandoffs = tool(\n 'read_handoffs',\n 'Read structured handoffs from earlier tasks. With no argument, returns the handoffs of your dependencies.',\n {\n type: z.string().optional(),\n taskId: z.string().optional(),\n },\n ((args: { type?: string; taskId?: string }) => {\n const handoffs = applyReadHandoffs(ctx, args);\n return textResult(JSON.stringify(handoffs, null, 2));\n }) as (args: never) => unknown,\n );\n\n return [enqueueTask, completeTask, readHandoffs];\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"queue-tools-BS8nXJ6S.js","names":["path"],"sources":["../src/shared/utils/atomic-ledger.ts","../src/agent/runner/sequence/orchestrator/queue.ts","../src/agent/runner/sequence/orchestrator/queue-tools.ts"],"sourcesContent":["/**\n * Small shared primitives for on-disk ledgers: an atomic JSON writer and a\n * single-chain async mutex. Used by the audit tools and by the orchestrator\n * queue. Lifted here so both share one implementation.\n */\nimport * as fs from 'fs';\n\n/**\n * Atomically write JSON: write to a `.tmp` file then rename over the target. The\n * rename bumps the file's mtime in one step, which is what a file watcher polls.\n */\nexport function writeJsonAtomic(targetPath: string, data: unknown): void {\n const tmpPath = `${targetPath}.tmp`;\n fs.writeFileSync(tmpPath, JSON.stringify(data, null, 2), 'utf8');\n fs.renameSync(tmpPath, targetPath);\n}\n\n/**\n * A single async mutex. Serializes read-modify-write cycles so concurrent callers\n * (parallel task agents, audit tool calls) never interleave a mutation.\n */\nexport function makeMutex() {\n let chain: Promise<unknown> = Promise.resolve();\n return async function run<T>(fn: () => Promise<T> | T): Promise<T> {\n const next = chain.then(() => fn());\n chain = next.catch(() => undefined);\n return next;\n };\n}\n","/**\n * The orchestrator task queue.\n *\n * In memory, synchronous, single-owner: one Node process drives the run, so\n * there is no locking. The queue imposes no execution policy — `nextRunnable`\n * returns every pending task whose dependencies are satisfied, and how many of\n * those run at once is decided by the task graph, not the queue.\n *\n * Every transition rewrites `<installDir>/.posthog-wizard-cache/queue.json`, a\n * small file holding the whole queue, handoffs included. It is the run's log\n * and the report's source. The whole cache folder is run-scoped and wiped when\n * the run ends.\n */\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { randomUUID } from 'crypto';\nimport { writeJsonAtomic } from '@utils/atomic-ledger';\nimport { analytics } from '@utils/analytics';\n\nexport const TaskStatus = {\n Pending: 'pending',\n Running: 'running',\n Done: 'done',\n Skipped: 'not needed',\n Failed: 'failed',\n} as const;\n\nexport type TaskStatus = (typeof TaskStatus)[keyof typeof TaskStatus];\n\n/**\n * Why a task ended as skipped.\n *\n * A skip has several causes that look identical from outside: an agent finding\n * the step does not apply, a user declining it, and an offer nobody answered.\n * They mean opposite things — the first is a no-op, the last two are the user\n * telling us something — and telling them apart needs the reason recorded at\n * the moment of the skip, not inferred later from timings.\n *\n * Recorded on the task like `error` is on a failed one, so it reaches the\n * skipped-task event and the run's queue.json alike.\n */\nexport const SkipReason = {\n /** The user answered the step's notice with Skip. */\n UserDeclined: 'user-declined',\n /** The notice went unanswered until it timed out. */\n NoticeTimeout: 'notice-timeout',\n /** The notice could not be shown at all, so consent failed closed. */\n NoticeError: 'notice-error',\n /** The task agent reported `not needed`. See {@link NotNeededReason} for why. */\n AgentNotNeeded: 'agent-not-needed',\n} as const;\n\nexport type SkipReason = (typeof SkipReason)[keyof typeof SkipReason];\n\n/**\n * Why the agent itself reported `not needed`.\n *\n * {@link SkipReason.AgentNotNeeded} records who decided the skip, not what they\n * decided. `complete_task` offers `not needed` both for \"the step does not\n * apply\" and for \"you cannot do it\", so an agent that never got a credential\n * out of the user lands in the same bucket as one that found nothing to\n * connect — and the bucket is named after the first meaning. For the one step\n * that stops to ask for credentials, that is the difference that matters: a\n * step nobody supplied a password for is not a step that did not apply.\n *\n * A sub-dimension of the skip reason, never a replacement for it, so every\n * existing reason value keeps counting exactly what it counted before.\n */\nexport const NotNeededReason = {\n /** Nothing to do: the step genuinely does not apply to this project. */\n NotApplicable: 'not-applicable',\n /** The user was asked and declined, or left the prompts unanswered. */\n UserDeclined: 'user-declined',\n /** Something outside the run stopped it — a credential, plan, or endpoint. */\n Blocked: 'blocked',\n} as const;\n\nexport type NotNeededReason =\n (typeof NotNeededReason)[keyof typeof NotNeededReason];\n\n/**\n * Whether a value names a reason. The pi harness hands tool arguments over\n * unvalidated, so an invented value would otherwise reach the analytics\n * dimension as free text — the one thing this step must never emit.\n */\nexport function isNotNeededReason(value: unknown): value is NotNeededReason {\n return (Object.values(NotNeededReason) as unknown[]).includes(value);\n}\n\nexport interface QueuedTask {\n id: string;\n type: string;\n /** Human-readable label for the TUI, set by the enqueuing agent. */\n label?: string;\n status: TaskStatus;\n /**\n * Ids of tasks that must finish before this one runs. Ids are generated at\n * enqueue, so a task can only depend on tasks created before it — the graph is\n * a DAG by construction, cycles cannot form. Unknown ids are rejected by the\n * enqueue_task guard.\n *\n * One sanctioned exception: {@link QueueStore.addDependencies} adds edges to a\n * still-pending task, for a runner-seeded task that was queued before the\n * planner ran and so could not name its dependencies then. It is additive,\n * pending-only, and cycle-checked per edge, so the DAG property above still\n * holds. Nothing else mutates this field.\n */\n dependsOn: string[];\n inputs: Record<string, unknown>;\n model?: string;\n attempts: number;\n maxAttempts: number;\n /** The structured handoff the task reported on completion. */\n handoff?: TaskHandoff;\n /** 'orchestrator' for seeded tasks, or the id of the task that enqueued this one. */\n enqueuedBy: string;\n /** Terminal failure unblocks dependents and never fails the run. */\n optional?: boolean;\n createdAt: string;\n startedAt?: string;\n finishedAt?: string;\n error?: { type: string; message: string };\n /** Set when `status` is `not needed`: why. The failed-task `error` of a skip. */\n skipReason?: SkipReason;\n /** Set only for an agent-reported skip, and only when the agent declared it. */\n notNeededReason?: NotNeededReason;\n}\n\nexport interface QueueFile {\n version: 1;\n runId: string;\n tasks: QueuedTask[];\n}\n\nexport interface TaskOutcome {\n type: string;\n status: TaskStatus;\n optional: boolean;\n}\n\n/** The structured handoff a task leaves for the next agent. */\nexport interface TaskHandoff {\n goals: string;\n did: string;\n forNextAgent: string;\n filesTouched?: string[];\n /** How the agent knows it worked — what it ran or observed. */\n evidence?: string;\n /** What the agent assumed about the app and could not verify. */\n assumptions?: string;\n /** A one-line summary of any unresolved conflict, surfaced in the outro. */\n conflict?: string;\n /** A finished section for the run's report, written by the task that owns the subject. */\n reportSection?: string;\n}\n\nexport interface EnqueueInput {\n type: string;\n label?: string;\n inputs?: Record<string, unknown>;\n dependsOn?: string[];\n model?: string;\n maxAttempts?: number;\n enqueuedBy?: string;\n optional?: boolean;\n}\n\nexport const QUEUE_DIR_NAME = '.posthog-wizard-cache';\nconst DEFAULT_MAX_ATTEMPTS = 2;\n\nfunction nowIso(): string {\n return new Date().toISOString();\n}\n\n/** Dropped in the cache folder so an orphaned copy explains itself. */\nconst DELETE_ME_FILE = '.DELETE-ME.md';\nconst DELETE_ME_BODY = `# Safe to delete\n\nThis folder contains run artifacts from the PostHog Wizard. This should have\nbeen deleted if the Wizard has finished running. If this wasn't deleted for\nsome reason, you can safely delete the entire \\`${QUEUE_DIR_NAME}/\\` folder.\n`;\n\n/** Every queue transition, in the order it is reflected. */\nexport type TransitionEvent =\n | 'enqueue'\n | 'start'\n | 'complete'\n | 'skip'\n | 'fail'\n | 'requeue';\n\nexport interface QueueStoreOptions {\n /**\n * Called on every transition with the task's post-transition state. The\n * runner uses it for telemetry; the store itself stays analytics-free.\n * Listener errors are reported but cannot break a transition.\n */\n onTransition?: (event: TransitionEvent, task: QueuedTask) => void;\n}\n\nexport class QueueStore {\n private tasks: QueuedTask[] = [];\n private readonly onTransition?: (\n event: TransitionEvent,\n task: QueuedTask,\n ) => void;\n\n readonly runId: string;\n readonly queuePath: string;\n\n constructor(installDir: string, runId: string, opts?: QueueStoreOptions) {\n this.onTransition = opts?.onTransition;\n this.runId = runId;\n const dir = path.join(installDir, QUEUE_DIR_NAME);\n this.queuePath = path.join(dir, 'queue.json');\n fs.mkdirSync(dir, { recursive: true });\n fs.writeFileSync(path.join(dir, DELETE_ME_FILE), DELETE_ME_BODY);\n }\n\n // ── Reads ───────────────────────────────────────────────────────────\n\n list(): readonly QueuedTask[] {\n return this.tasks;\n }\n\n get(id: string): QueuedTask | undefined {\n return this.tasks.find((t) => t.id === id);\n }\n\n /**\n * Every pending task whose dependencies are all satisfied (`done` or\n * `skipped`). A skipped dependency does not block downstream work.\n */\n nextRunnable(): QueuedTask[] {\n // A TERMINALLY failed optional dep satisfies like skipped; retryable failure still blocks.\n const doneIds = new Set(\n this.tasks\n .filter(\n (t) =>\n t.status === TaskStatus.Done ||\n t.status === TaskStatus.Skipped ||\n (t.status === TaskStatus.Failed &&\n t.optional === true &&\n t.attempts >= t.maxAttempts),\n )\n .map((t) => t.id),\n );\n return this.tasks.filter(\n (t) =>\n t.status === TaskStatus.Pending &&\n t.dependsOn.every((d) => doneIds.has(d)),\n );\n }\n\n /**\n * True when no task is running and none can be started. Either everything\n * is terminal, or the only pending tasks are blocked by a failed dependency.\n */\n isDrained(): boolean {\n if (this.tasks.some((t) => t.status === TaskStatus.Running)) return false;\n return this.nextRunnable().length === 0;\n }\n\n summary(): Record<TaskStatus, number> & { total: number } {\n const counts: Record<TaskStatus, number> = {\n [TaskStatus.Pending]: 0,\n [TaskStatus.Running]: 0,\n [TaskStatus.Done]: 0,\n [TaskStatus.Skipped]: 0,\n [TaskStatus.Failed]: 0,\n };\n for (const t of this.tasks) counts[t.status] += 1;\n return { ...counts, total: this.tasks.length };\n }\n\n readHandoff(id: string): TaskHandoff | null {\n return this.get(id)?.handoff ?? null;\n }\n\n /** Handoffs of completed tasks of a given type, oldest first. */\n readHandoffsByType(type: string): TaskHandoff[] {\n return this.tasks\n .filter((t) => t.type === type && t.handoff)\n .map((t) => t.handoff as TaskHandoff);\n }\n\n // ── Transitions (each one reflected to queue.json) ──────────────────\n\n enqueue(input: EnqueueInput): QueuedTask {\n const task: QueuedTask = {\n id: randomUUID(),\n type: input.type,\n label: input.label,\n status: TaskStatus.Pending,\n dependsOn: input.dependsOn ?? [],\n inputs: input.inputs ?? {},\n model: input.model,\n attempts: 0,\n maxAttempts: input.maxAttempts ?? DEFAULT_MAX_ATTEMPTS,\n enqueuedBy: input.enqueuedBy ?? 'orchestrator',\n optional: input.optional,\n createdAt: nowIso(),\n };\n this.tasks.push(task);\n this.reflect();\n this.notify('enqueue', task);\n return task;\n }\n\n /**\n * Add dependency edges to a pending task — the one sanctioned exception to\n * `dependsOn` being immutable (see the field's doc).\n *\n * A runner-seeded task is enqueued before the planner runs, so the tasks it\n * should wait for do not exist yet and it cannot name them. This closes that\n * gap once, between planning and the drain.\n *\n * Three rules keep the graph a DAG: additive only (never removes an edge),\n * pending only (a task that already started keeps the graph it ran under), and\n * every edge cycle-checked — a dep that already reaches this task through its\n * own chain is refused. Refusals come back in the result rather than throwing:\n * one bad edge is a bug worth reporting, not a reason to end a run that is\n * otherwise fine.\n */\n addDependencies(\n id: string,\n depIds: readonly string[],\n ): { added: string[]; refused: string[] } {\n const task = this.require(id);\n if (task.status !== TaskStatus.Pending) {\n return { added: [], refused: [...depIds] };\n }\n\n const added: string[] = [];\n const refused: string[] = [];\n for (const depId of depIds) {\n // Already an edge, or a self-loop: nothing to do and nothing wrong.\n if (depId === id || task.dependsOn.includes(depId)) continue;\n // Checked against the graph as it stands, so two edges added in one call\n // cannot together form a cycle the first check missed.\n if (!this.get(depId) || this.reaches(depId, id)) {\n refused.push(depId);\n continue;\n }\n task.dependsOn.push(depId);\n added.push(depId);\n }\n if (added.length > 0) this.reflect();\n return { added, refused };\n }\n\n start(id: string): QueuedTask {\n const t = this.require(id);\n t.status = TaskStatus.Running;\n t.startedAt = nowIso();\n t.attempts += 1;\n this.reflect();\n this.notify('start', t);\n return t;\n }\n\n complete(id: string, handoff?: TaskHandoff): QueuedTask {\n return this.finish(id, TaskStatus.Done, handoff);\n }\n\n /**\n * Terminal: the task was not done, and that is not a failure.\n *\n * The reason is required, and sits before the optional handoff for that\n * reason. A skip carrying no reason is what let a five-minute auto-decline\n * hide inside the same event as an agent deciding a step did not apply.\n *\n * `notNeededReason` splits the agent-reported reason one level further; only\n * an agent supplies it, and only when it declared one.\n */\n skip(\n id: string,\n reason: SkipReason,\n handoff?: TaskHandoff,\n notNeededReason?: NotNeededReason,\n ): QueuedTask {\n const t = this.require(id);\n t.skipReason = reason;\n if (notNeededReason) t.notNeededReason = notNeededReason;\n return this.finish(id, TaskStatus.Skipped, handoff);\n }\n\n fail(\n id: string,\n error: { type: string; message: string },\n handoff?: TaskHandoff,\n ): QueuedTask {\n const t = this.require(id);\n t.error = error;\n return this.finish(id, TaskStatus.Failed, handoff);\n }\n\n /** Put a failed/running task back to pending for a retry within the run. */\n requeue(id: string): QueuedTask {\n const t = this.require(id);\n t.status = TaskStatus.Pending;\n t.startedAt = undefined;\n t.finishedAt = undefined;\n this.reflect();\n this.notify('requeue', t);\n return t;\n }\n\n // ── Internals ───────────────────────────────────────────────────────\n\n private finish(\n id: string,\n status: 'done' | 'not needed' | 'failed',\n handoff?: TaskHandoff,\n ): QueuedTask {\n const t = this.require(id);\n if (handoff) t.handoff = handoff;\n t.status = status;\n t.finishedAt = nowIso();\n this.reflect();\n this.notify(\n status === TaskStatus.Done\n ? 'complete'\n : status === TaskStatus.Skipped\n ? 'skip'\n : 'fail',\n t,\n );\n return t;\n }\n\n private reflect(): void {\n const file: QueueFile = {\n version: 1,\n runId: this.runId,\n tasks: this.tasks,\n };\n writeJsonAtomic(this.queuePath, file);\n }\n\n private notify(event: TransitionEvent, task: QueuedTask): void {\n try {\n this.onTransition?.(event, task);\n } catch (error) {\n // A listener must never break a transition, but its failure is a bug.\n analytics.captureException(\n error instanceof Error ? error : new Error(String(error)),\n { step: 'orchestrator_queue_listener', event },\n );\n }\n }\n\n /** Whether `fromId` reaches `targetId` by following dependsOn edges. */\n private reaches(fromId: string, targetId: string): boolean {\n const seen = new Set<string>();\n const stack = [fromId];\n while (stack.length > 0) {\n const current = stack.pop() as string;\n if (current === targetId) return true;\n if (seen.has(current)) continue;\n seen.add(current);\n stack.push(...(this.get(current)?.dependsOn ?? []));\n }\n return false;\n }\n\n private require(id: string): QueuedTask {\n const t = this.get(id);\n if (!t) throw new Error(`No task ${id} in the queue`);\n return t;\n }\n}\n","/**\n * Orchestrator MCP tools, registered into the existing `wizard-tools` server when\n * a queue is present. They let the orchestrator agent and task agents grow the\n * queue, report completion with a structured handoff, and read prior handoffs.\n *\n * The guard logic and the apply functions are plain, exported, and unit-tested.\n * `buildOrchestratorTools` wraps them in the SDK `tool()` shape.\n */\nimport { z } from 'zod';\nimport { analytics } from '@utils/analytics';\nimport { redactSecrets } from '@shared/mcp-clients/results';\nimport { isValidModel, VALID_MODELS } from '../../switchboard/models';\nimport {\n isNotNeededReason,\n NotNeededReason,\n SkipReason,\n TaskStatus,\n type QueueStore,\n type QueuedTask,\n type TaskHandoff,\n} from './queue';\n\n/**\n * The `complete_task` `notNeededReason` description, shared by both harnesses'\n * schemas so the ask cannot drift between them.\n *\n * Named per value rather than left to prose: the distinction the flow needs is\n * \"the user did not hand over a credential\" against \"there was nothing here to\n * connect\", and an agent asked for that in free text writes it into the handoff\n * — where {@link applyComplete} deliberately cannot forward it, because handoff\n * prose on this step reaches live database passwords.\n */\nexport const NOT_NEEDED_REASON_ASK = `Required when status is 'not needed': which of these ended the task. '${NotNeededReason.NotApplicable}' — the step genuinely does not apply to this project. '${NotNeededReason.UserDeclined}' — you asked the user and they declined, cancelled, or never answered. '${NotNeededReason.Blocked}' — something outside your and the user's control stopped you (a credential the project does not have, a plan or permission it lacks, an endpoint you could not reach). Ignored for any other status.`;\n\n/**\n * The `enqueue_task` `model` description, shared by both harnesses' schemas.\n *\n * The field was declared as a bare optional string while\n * {@link checkEnqueueGuards} rejects anything outside {@link VALID_MODELS}, so\n * the allow-list existed only in the rejection message — an agent had to guess\n * a gateway model id, trip the `invalid-model` guard, and spend a turn reading\n * the list back before it could enqueue. Naming the list where the agent picks\n * the value costs nothing and makes the guess unnecessary.\n */\nexport const ENQUEUE_MODEL_DESCRIPTION = `Optional model override for this task. Omit it to use the task's default, which is almost always right. If you do set it, it must be one of: ${[\n ...VALID_MODELS,\n].join(', ')}.`;\n\n/**\n * The `complete_task` tool description, shared by both harnesses' schemas so\n * the outcome contract cannot drift — the same discipline {@link HANDOFF_FIELDS}\n * applies to the fields inside the handoff.\n *\n * The closing sentence names the nesting because agents kept learning it from a\n * rejection instead: a first call with `goals`/`did`/`forNextAgent` at the top\n * level fails schema validation, and reading that error back was the only place\n * the shape was stated. Naming it where the agent fills the call in costs\n * nothing, the same reasoning as {@link ENQUEUE_MODEL_DESCRIPTION}.\n */\nexport const COMPLETE_TASK_DESCRIPTION =\n 'Report the outcome of your task. Always call this exactly once when you finish, with a structured handoff for the next agent. ' +\n \"Use status 'not needed' when the task does not apply to this project and you cannot do it (say why in the handoff) — not 'done'. \" +\n 'The handoff is one nested object: put `goals`, `did` and `forNextAgent` — all three required — inside `handoff`, along with any optional fields, never at the top level.';\n\n/** The per-task remark ask, shared by both harnesses' complete_task schemas. */\nexport const REMARK_ASK =\n 'What information or guidance would have been useful to have in the integration prompt or documentation for this task — specifically anything that would have prevented tool failures, erroneous edits, or other wasted turns.';\n\nexport interface OrchestratorToolsContext {\n store: QueueStore;\n /** Task types the registry knows about. enqueue_task rejects anything else. */\n validTypes: readonly string[];\n /**\n * Types marked `sink: true` — the ones that run last and must therefore\n * depend, transitively, on every other task in the queue. Enqueuing one with\n * an incomplete closure is rejected, so a task the runner seeded before the\n * planner ran can never be left un-awaited.\n */\n sinkTypes?: readonly string[];\n /**\n * Types the wizard queues itself, before the planner runs. They are deferred\n * to the end of the drain and may stop to ask the user for input, so the edge\n * to them is one-way: the sink depends on them, nothing else may. See\n * {@link seededDepViolations}.\n */\n runnerSeededTypes?: readonly string[];\n /**\n * Types marked `optional: true` in their frontmatter. Enqueue stamps the flag\n * from this list — the task's definition decides, never the enqueuing agent —\n * so terminal failure of such a task unblocks dependents and never fails the\n * run.\n */\n optionalTypes?: readonly string[];\n /**\n * The id of the task this tool server is bound to. Each task agent gets its\n * own wizard-tools server, so attribution holds when independent tasks run\n * in parallel. Absent for the seed, which is not a task.\n */\n currentTaskId?: string;\n}\n\n/**\n * Whether the task this context is bound to has reported through\n * `complete_task`. False for the seed, which is not a task.\n */\nexport function taskReported(ctx: OrchestratorToolsContext): boolean {\n if (!ctx.currentTaskId) return false;\n const status = ctx.store.get(ctx.currentTaskId)?.status;\n return (\n status === TaskStatus.Done ||\n status === TaskStatus.Failed ||\n status === TaskStatus.Skipped\n );\n}\n\nexport interface EnqueueArgs {\n type: string;\n label?: string;\n inputs?: Record<string, unknown>;\n dependsOn?: string[];\n model?: string;\n reason: string;\n}\n\nexport type GuardResult =\n | { ok: true }\n | { ok: false; guard: string; message: string };\n\nfunction stableStringify(value: unknown): string {\n if (value === null || typeof value !== 'object') return JSON.stringify(value);\n if (Array.isArray(value)) return `[${value.map(stableStringify).join(',')}]`;\n const entries = Object.entries(value as Record<string, unknown>).sort(\n ([a], [b]) => a.localeCompare(b),\n );\n return `{${entries\n .map(([k, v]) => `${JSON.stringify(k)}:${stableStringify(v)}`)\n .join(',')}}`;\n}\n\nfunction dedupKey(type: string, inputs: Record<string, unknown>): string {\n return `${type}::${stableStringify(inputs)}`;\n}\n\n/**\n * A backstop on total queue size. Tasks can enqueue tasks, so a misbehaving\n * type could grow the queue without bound. Keeping the graph small is the job\n * of good agent and skill design, not this number — it only stops a runaway.\n * The real flow is ~9 tasks, so this sits well clear of it.\n */\nconst MAX_QUEUE_TASKS = 30;\n\n/** Every task id reachable from `roots` through dependsOn edges, roots included. */\nexport function dependencyClosure(\n store: QueueStore,\n roots: readonly string[],\n): Set<string> {\n const seen = new Set<string>();\n const visit = (id: string): void => {\n if (seen.has(id)) return;\n seen.add(id);\n for (const dep of store.get(id)?.dependsOn ?? []) visit(dep);\n };\n for (const id of roots) visit(id);\n return seen;\n}\n\n/**\n * Tasks a sink would fail to wait for. A sink runs last by definition, so any\n * queued task outside its dependency closure is work whose handoff the sink\n * would miss — the exact failure mode of a task seeded before the planner ran.\n * Empty for a non-sink type, and for a sink that covers everything.\n */\nexport function uncoveredBySink(\n ctx: OrchestratorToolsContext,\n args: Pick<EnqueueArgs, 'type' | 'dependsOn'>,\n): QueuedTask[] {\n if (!ctx.sinkTypes?.includes(args.type)) return [];\n const covered = dependencyClosure(ctx.store, args.dependsOn ?? []);\n return ctx.store.list().filter((t) => !covered.has(t.id));\n}\n\n/**\n * Runner-seeded tasks a non-sink task would end up waiting for.\n *\n * The edge to a runner-seeded task is one-way. Such a task is deferred to the\n * end of the drain and may stop to ask the user for input, so anything that\n * waits on it inherits that wait — a coding step gated on someone typing a\n * database password. The sink is the sole exception: it runs last by\n * definition and must depend on every task, this one included.\n *\n * The whole closure is checked, not just direct dependencies, so the rule\n * cannot be reached through an intermediate hop.\n *\n * Empty for a sink, and for any task that does not reach a seeded task.\n */\nexport function seededDepViolations(\n ctx: OrchestratorToolsContext,\n args: Pick<EnqueueArgs, 'type' | 'dependsOn'>,\n): QueuedTask[] {\n const seeded = ctx.runnerSeededTypes ?? [];\n if (seeded.length === 0) return [];\n if (ctx.sinkTypes?.includes(args.type)) return [];\n const reached = dependencyClosure(ctx.store, args.dependsOn ?? []);\n return ctx.store\n .list()\n .filter((t) => reached.has(t.id) && seeded.includes(t.type));\n}\n\n/**\n * Validate an enqueue. Structural checks only — a real type, real dependencies,\n * a sink that waits for everything, no non-sink task waiting on a runner-seeded\n * one, not a literal duplicate, and not past the runaway backstop. How much\n * runs, and in what shape, is the task graph's business, not a knob's.\n */\nexport function checkEnqueueGuards(\n ctx: OrchestratorToolsContext,\n args: EnqueueArgs,\n): GuardResult {\n const tasks = ctx.store.list();\n\n if (tasks.length >= MAX_QUEUE_TASKS) {\n return {\n ok: false,\n guard: 'queue-full',\n message: `The queue already holds ${tasks.length} tasks (cap ${MAX_QUEUE_TASKS}). Refine the existing tasks rather than adding more.`,\n };\n }\n\n if (!ctx.validTypes.includes(args.type)) {\n return {\n ok: false,\n guard: 'unknown-type',\n message: `Unknown task type \"${\n args.type\n }\". Valid types: ${ctx.validTypes.join(', ')}.`,\n };\n }\n\n // Reject an agent pinning a task to a non-allow-listed model.\n if (args.model !== undefined && !isValidModel(args.model)) {\n return {\n ok: false,\n guard: 'invalid-model',\n message: `Model \"${\n args.model\n }\" is not allowed. Omit model to use the task default, or pick one of: ${[\n ...VALID_MODELS,\n ].join(', ')}.`,\n };\n }\n\n for (const dep of args.dependsOn ?? []) {\n if (!ctx.store.get(dep)) {\n return {\n ok: false,\n guard: 'unknown-dep',\n message: `Dependency \"${dep}\" is not a known task id.`,\n };\n }\n }\n\n const seededDeps = seededDepViolations(ctx, args);\n if (seededDeps.length > 0) {\n return {\n ok: false,\n guard: 'seeded-dep',\n message: `A \"${args.type}\" task must not wait for ${seededDeps\n .map((t) => `${t.type} (${t.id})`)\n .join(\n ', ',\n )} — directly or through its dependencies. The wizard placed those, runs them at the end, and they stop to ask the user for input, so anything waiting on one waits on a person. Remove them from dependsOn and enqueue it again; only the final reporting task may depend on them.`,\n };\n }\n\n const uncovered = uncoveredBySink(ctx, args);\n if (uncovered.length > 0) {\n const seededUncovered = uncovered.filter((t) =>\n (ctx.runnerSeededTypes ?? []).includes(t.type),\n );\n // \"Or to a task already in dependsOn\" is the right advice for an ordinary\n // task and exactly wrong for a runner-seeded one — routing it through an\n // intermediate satisfies the closure while breaking the one-way rule above.\n // So a seeded task is named with the only placement that is legal for it.\n const how =\n seededUncovered.length === uncovered.length\n ? \"Add them to this task's own dependsOn — for a task the wizard placed, that is the only legal spot — and enqueue it again.\"\n : 'Add them to dependsOn, or to a task already in dependsOn, and enqueue it again.';\n return {\n ok: false,\n guard: 'sink-closure',\n message: `A \"${\n args.type\n }\" task runs last, so it must depend on every other task — directly or through its dependencies. These are not covered: ${uncovered\n .map((t) => `${t.type} (${t.id})`)\n .join(', ')}. ${how}`,\n };\n }\n\n const key = dedupKey(args.type, args.inputs ?? {});\n if (\n tasks.some(\n (t) =>\n t.status !== TaskStatus.Failed && dedupKey(t.type, t.inputs) === key,\n )\n ) {\n return {\n ok: false,\n guard: 'dedup',\n message: `A \"${args.type}\" task with these inputs already exists.`,\n };\n }\n\n return { ok: true };\n}\n\nexport type EnqueueResult =\n | { ok: true; task: QueuedTask }\n | { ok: false; guard: string; message: string };\n\nexport function applyEnqueue(\n ctx: OrchestratorToolsContext,\n args: EnqueueArgs,\n): EnqueueResult {\n const guard = checkEnqueueGuards(ctx, args);\n if (!guard.ok) return guard;\n\n const task = ctx.store.enqueue({\n type: args.type,\n label: args.label,\n inputs: args.inputs ?? {},\n dependsOn: args.dependsOn ?? [],\n model: args.model,\n optional: (ctx.optionalTypes ?? []).includes(args.type) || undefined,\n enqueuedBy: ctx.currentTaskId ?? 'orchestrator',\n });\n return { ok: true, task };\n}\n\nexport type CompleteResult = { ok: true } | { ok: false; message: string };\n\nexport type CompleteArgs = {\n status: 'done' | 'failed' | 'not needed';\n handoff: TaskHandoff;\n remark?: string;\n notNeededReason?: NotNeededReason;\n};\n\nexport function applyComplete(\n ctx: OrchestratorToolsContext,\n args: CompleteArgs,\n): CompleteResult {\n const id = ctx.currentTaskId;\n if (!id) {\n return {\n ok: false,\n message: 'complete_task can only be called by a running task agent.',\n };\n }\n if (args.remark) {\n analytics.wizardCapture('orchestrator remark', {\n task_type: ctx.store.get(id)?.type,\n remark: redactSecrets(args.remark),\n });\n }\n if (args.status === TaskStatus.Failed) {\n ctx.store.fail(\n id,\n { type: 'self-reported', message: args.handoff.forNextAgent },\n args.handoff,\n );\n } else if (args.status === TaskStatus.Skipped) {\n // The agent's own words stay in the handoff and out of telemetry. This flow\n // reaches live database and API credentials, and the repo has no redaction\n // pass for handoff prose, so the event carries the task type, the reason,\n // and the closed set of `notNeededReason` values — enough to separate an\n // agent no-op from a user decline from a blocked step, with no free text.\n ctx.store.skip(\n id,\n SkipReason.AgentNotNeeded,\n args.handoff,\n isNotNeededReason(args.notNeededReason)\n ? args.notNeededReason\n : undefined,\n );\n } else {\n ctx.store.complete(id, args.handoff);\n }\n return { ok: true };\n}\n\nexport function applyReadHandoffs(\n ctx: OrchestratorToolsContext,\n args: { type?: string; taskId?: string },\n): TaskHandoff[] {\n if (args.taskId) {\n const h = ctx.store.readHandoff(args.taskId);\n return h ? [h] : [];\n }\n if (args.type) {\n return ctx.store.readHandoffsByType(args.type);\n }\n // No filter: every handoff of a dependency of the current task.\n const currentId = ctx.currentTaskId;\n const current = currentId ? ctx.store.get(currentId) : undefined;\n if (!current) return [];\n return current.dependsOn\n .map((depId) => ctx.store.readHandoff(depId))\n .filter((h): h is TaskHandoff => h !== null);\n}\n\n// Caps each LLM-authored free-text field; queue.json rewrites whole per transition.\nconst HANDOFF_TEXT_MAX = 8_000;\n\n/**\n * The one description of every handoff field, shared by both harnesses'\n * `complete_task` schemas — the zod shape below and the typebox mirror in\n * `harness/pi/orchestrator-tools.ts`. A field an agent cannot see is a field it\n * cannot fill, so the two drifting silently drops whatever the missing field\n * carried; `__tests__/handoff-schema-parity.test.ts` holds them level.\n */\nexport const HANDOFF_FIELDS = {\n goals: 'What this task was asked to achieve.',\n did: 'What you actually did — for each file you edited: the change, the intention behind it, and the analytics it should feed (the insight, funnel, or dashboard tile it becomes part of).',\n forNextAgent: 'What the next agent should know.',\n filesTouched: 'Paths of every file you edited.',\n evidence:\n 'How you know it worked — what you ran or observed, not what you expect.',\n assumptions: 'What you assumed about the app and could not verify.',\n conflict:\n 'A one-line summary of any conflict you could not cleanly resolve (e.g. a dependency or build conflict). Put full detail in your work; this line is surfaced to the user.',\n reportSection:\n 'A finished markdown section about your work for the run report, written only when your task instructions ask for one. The reporting task includes it as its own section instead of rewriting it.',\n} as const satisfies Record<keyof Required<TaskHandoff>, string>;\n\nconst HANDOFF_SHAPE = {\n goals: z.string().max(HANDOFF_TEXT_MAX).describe(HANDOFF_FIELDS.goals),\n did: z.string().max(HANDOFF_TEXT_MAX).describe(HANDOFF_FIELDS.did),\n forNextAgent: z\n .string()\n .max(HANDOFF_TEXT_MAX)\n .describe(HANDOFF_FIELDS.forNextAgent),\n filesTouched: z\n .array(z.string().max(1_000))\n .max(200)\n .optional()\n .describe(HANDOFF_FIELDS.filesTouched),\n evidence: z\n .string()\n .max(HANDOFF_TEXT_MAX)\n .optional()\n .describe(HANDOFF_FIELDS.evidence),\n assumptions: z\n .string()\n .max(HANDOFF_TEXT_MAX)\n .optional()\n .describe(HANDOFF_FIELDS.assumptions),\n conflict: z\n .string()\n .max(HANDOFF_TEXT_MAX)\n .optional()\n .describe(HANDOFF_FIELDS.conflict),\n reportSection: z\n .string()\n .max(HANDOFF_TEXT_MAX)\n .optional()\n .describe(HANDOFF_FIELDS.reportSection),\n};\n\n/** Exported so the parity test can compare both harnesses' field sets. */\nexport const HANDOFF_SHAPE_KEYS: readonly string[] = Object.keys(HANDOFF_SHAPE);\n\n/**\n * `complete_task`'s own arguments, held level with the pi mirror by the same\n * parity test that guards the handoff — a top-level field can go missing on the\n * harness that runs just as easily as a nested one.\n */\nconst COMPLETE_SHAPE = {\n status: z.enum(['done', 'failed', 'not needed']),\n handoff: z.object(HANDOFF_SHAPE),\n remark: z.string().optional().describe(REMARK_ASK),\n notNeededReason: z\n .enum([\n NotNeededReason.NotApplicable,\n NotNeededReason.UserDeclined,\n NotNeededReason.Blocked,\n ])\n .optional()\n .describe(NOT_NEEDED_REASON_ASK),\n};\n\n/** Exported so the parity test can compare both harnesses' field sets. */\nexport const COMPLETE_SHAPE_KEYS: readonly string[] =\n Object.keys(COMPLETE_SHAPE);\n\ntype SdkTool = (\n name: string,\n description: string,\n // The SDK accepts a plain object of zod fields as the schema.\n schema: Record<string, z.ZodTypeAny>,\n handler: (args: never) => unknown,\n) => unknown;\n\nfunction textResult(text: string, isError = false) {\n return { isError, content: [{ type: 'text' as const, text }] };\n}\n\n/**\n * Build the orchestrator tools in the SDK `tool()` shape. Called from\n * createWizardToolsServer only when a queue context is present.\n */\nexport function buildOrchestratorTools(\n tool: SdkTool,\n ctx: OrchestratorToolsContext,\n): unknown[] {\n const enqueueTask = tool(\n 'enqueue_task',\n 'Add a task to the orchestrator queue. Use it to seed work and to enqueue follow-up work you discover. Keep tasks small and discrete.',\n {\n type: z\n .string()\n .describe(`The task type. One of: ${ctx.validTypes.join(', ')}.`),\n label: z\n .string()\n .optional()\n .describe(\n 'A short label for the UI — the action in a few words (e.g. \"Add the PostHog SDK\", \"Initialize PostHog\"). Leave out file names, class names, and other specifics.',\n ),\n inputs: z.record(z.unknown()).optional(),\n dependsOn: z\n .array(z.string())\n .optional()\n .describe('Task ids that must be done before this task runs.'),\n model: z.string().optional().describe(ENQUEUE_MODEL_DESCRIPTION),\n reason: z.string().describe('One line on why this task is needed.'),\n },\n ((args: EnqueueArgs) => {\n const res = applyEnqueue(ctx, args);\n if (!res.ok) {\n analytics.wizardCapture('orchestrator guard tripped', {\n guard: res.guard,\n type: args.type,\n });\n return textResult(res.message, true);\n }\n return textResult(JSON.stringify({ id: res.task.id }));\n }) as (args: never) => unknown,\n );\n\n const completeTask = tool(\n 'complete_task',\n COMPLETE_TASK_DESCRIPTION,\n COMPLETE_SHAPE,\n ((args: CompleteArgs) => {\n const res = applyComplete(ctx, args);\n if (!res.ok) return textResult(res.message, true);\n return textResult('ok');\n }) as (args: never) => unknown,\n );\n\n const readHandoffs = tool(\n 'read_handoffs',\n 'Read structured handoffs from earlier tasks. With no argument, returns the handoffs of your dependencies.',\n {\n type: z.string().optional(),\n taskId: z.string().optional(),\n },\n ((args: { type?: string; taskId?: string }) => {\n const handoffs = applyReadHandoffs(ctx, args);\n return textResult(JSON.stringify(handoffs, null, 2));\n }) as (args: never) => unknown,\n );\n\n return [enqueueTask, completeTask, readHandoffs];\n}\n"],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as analytics } from "./analytics-
|
|
1
|
+
import { n as analytics } from "./analytics-J1cjIekQ.js";
|
|
2
2
|
//#region src/shared/mcp-clients/results.ts
|
|
3
3
|
/**
|
|
4
4
|
* Failure text comes from CLIs we invoke with the user's personal API key on the
|
|
@@ -87,4 +87,4 @@ const namesWithStatus = (results, status) => results.filter((r) => r.status ===
|
|
|
87
87
|
//#endregion
|
|
88
88
|
export { scrubHomePaths as a, reportSpawnFailure as i, namesWithStatus as n, summarizeFailure as o, redactSecrets as r, toClientResult as s, isOk as t };
|
|
89
89
|
|
|
90
|
-
//# sourceMappingURL=results-
|
|
90
|
+
//# sourceMappingURL=results-BE1ctlKP.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"results-5yCKf2cI.js","names":[],"sources":["../src/shared/mcp-clients/results.ts"],"sourcesContent":["import { analytics } from '@utils/analytics';\n\n/**\n * Per-client outcome of an MCP server / plugin install or removal.\n *\n * \"Nothing to do\" is a first-class outcome rather than a silent no-op:\n * re-running `mcp add` on a machine that's already set up is the common case,\n * and collapsing it into an empty result set is what made the flow report\n * \"Installation skipped.\" with no explanation. Failures carry a short reason\n * for the same reason — an empty list tells the user nothing.\n *\n * The names are action-neutral because both `mcp add` and `mcp remove` report\n * through them: `Changed` is \"installed\" or \"removed\" depending on the flow.\n */\n\nexport enum McpClientStatus {\n /** We made the change — wrote the config, installed or removed the server. */\n Changed = 'changed',\n /** Already in the requested state; nothing was touched. */\n Unchanged = 'unchanged',\n Failed = 'failed',\n}\n\nexport interface McpClientResult {\n name: string;\n status: McpClientStatus;\n /** Short, user-facing explanation. Set for failures. */\n detail?: string;\n}\n\n/** Result shape every client's addServer/removeServer/installPlugin returns. */\nexport interface InstallResult {\n success: boolean;\n /** Already in the requested state — nothing was written or removed. */\n alreadyInstalled?: boolean;\n /** Raw failure text from the underlying CLI or filesystem error. */\n reason?: string;\n}\n\n/**\n * Failure text comes from CLIs we invoke with the user's personal API key on the\n * command line (`--header \"Authorization: Bearer phx_...\"`), so it can carry the\n * key into the log file, the TUI and exception reports. Mask it first.\n *\n * The plugin stages clone git repositories and read the user's own config, so\n * the text reaching here is no longer only ours: a failing clone echoes the\n * remote URL with any credentials embedded in it, and a config error can quote\n * a foreign provider's key. Orchestrator task remarks reach analytics through\n * here too, from a step that collects database and API credentials. Each\n * pattern below is a shape one of these sources can carry, not a guess at\n * every possible secret.\n */\nexport const redactSecrets = (raw: string): string =>\n raw\n .replace(/Bearer\\s+\\S+/gi, 'Bearer [redacted]')\n .replace(/ph[xspc]_[A-Za-z0-9_-]+/g, '[redacted]')\n // `https://user:token@github.com/...` — git prints the whole remote back;\n // `postgres://user:password@host/db` is the same shape for a database.\n .replace(/(\\b[a-z][a-z0-9+.-]*:\\/\\/)[^/\\s:@]+:[^/\\s@]+@/gi, '$1[redacted]@')\n // GitHub tokens, which a clone failure quotes verbatim.\n .replace(/\\bgh[pousr]_[A-Za-z0-9]{16,}/g, '[redacted]')\n .replace(/\\bgithub_pat_[A-Za-z0-9_]{20,}/g, '[redacted]')\n // OpenAI keys: `OPENAI_API_KEY` is already a failure codex reports on.\n .replace(/\\bsk-[A-Za-z0-9_-]{16,}/g, '[redacted]')\n // Stripe secret and restricted keys, which the warehouse step collects.\n .replace(/\\b[rs]k_(?:live|test)_[A-Za-z0-9]{16,}/g, '[redacted]')\n // Google keys, which a config error quotes the same way.\n .replace(/\\bAIza[A-Za-z0-9_-]{16,}/g, '[redacted]')\n // The catch-all for providers we have no shape for: a config error prints\n // the offending line, and the variable's own name says it holds a secret.\n // The name is kept — `OPENAI_API_KEY` is wording an expected-failure hint\n // matches on, and masking it would turn that hint back into an exception.\n .replace(\n /\\b([A-Za-z0-9_]*(?:KEY|TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIALS?)[A-Za-z0-9_]*)(\\s*[=:]\\s*)[\"']?[A-Za-z0-9_\\-./+]{8,}[\"']?/gi,\n '$1$2[redacted]',\n );\n\n/**\n * Replace the user's home directory with `~`. Error tracking fingerprints on\n * the message, so a path that differs per user splits one root cause into one\n * issue per user — and the username itself never needs to leave the machine.\n */\nexport const scrubHomePaths = (raw: string): string =>\n raw\n .replace(/\\/(?:Users|home)\\/[^/\\s'\"]+/g, '~')\n .replace(/[A-Za-z]:\\\\Users\\\\[^\\\\\\s'\"]+/gi, '~');\n\n/**\n * A failure that lives entirely in the user's environment: a CLI too old, a\n * broken install, an unreadable config. We can't fix these, so we hand back a\n * hint instead of filing an issue nobody can action.\n *\n * Silencing a failure is the expensive direction: a pattern that matches too\n * much retires a real bug of ours into a hint nobody reads. So an entry says\n * where it applies (`stages`) and what disqualifies it (`unless`), and anything\n * it cannot claim confidently goes back to being reported.\n */\nexport interface ExpectedFailure {\n match: RegExp;\n /**\n * Stages this hint is allowed to fire on. Omitted means any stage. Wording\n * that means one thing during `plugin install` can mean something else during\n * `mcp add`, and a hint naming the wrong command is worse than none.\n */\n stages?: string[];\n /**\n * Wording that disqualifies the match even when `match` hits — the tell that\n * the failure is ours, not the environment's.\n */\n unless?: RegExp;\n /** One short line, shown to the user as the failure detail. */\n hint: string;\n}\n\n/**\n * The first matching hint, or undefined when the failure is worth reporting.\n *\n * `stage` is the caller's name for what it was doing. Passing it lets an entry\n * narrow itself; omitting it only considers entries that claim every stage,\n * because an unscoped call cannot honour a scoped entry.\n */\nexport const expectedFailureHint = (\n text: string,\n table: ExpectedFailure[],\n stage?: string,\n): string | undefined => {\n if (!text.trim()) return undefined;\n return table.find(\n (f) =>\n (f.stages === undefined ||\n (stage !== undefined && f.stages.includes(stage))) &&\n f.match.test(text) &&\n !f.unless?.test(text),\n )?.hint;\n};\n\n/**\n * Turn a failed CLI call into a result: an expected local failure becomes a\n * hint, anything else is reported under a constant message so one root cause\n * stays one issue, with the varying detail in properties.\n *\n * Shared by every client so the hinted-event shape cannot drift between them.\n * `Stage` is the caller's union: a hint scoped to a stage nobody passes is\n * silently dead.\n */\nexport const reportSpawnFailure = <Stage extends string>(opts: {\n client: string;\n stage: Stage;\n details: string;\n table: ExpectedFailure[];\n /** Extra exception properties, e.g. a failure that travelled beside this one. */\n extra?: Record<string, unknown>;\n}): InstallResult => {\n const { client, stage, details, table, extra } = opts;\n const hint = expectedFailureHint(details, table, stage);\n if (hint) {\n // Hinting takes a failure out of error tracking, so without this the only\n // evidence a pattern has started over-matching is that our exception count\n // fell, which reads as the fix working. An event keeps the count.\n analytics.wizardCapture('mcp expected failure hinted', {\n client,\n stage,\n hint,\n details: scrubHomePaths(details),\n });\n return { success: false, reason: hint };\n }\n analytics.captureException(new Error(`${client} ${stage} failed`), {\n stage,\n details: scrubHomePaths(details),\n ...extra,\n });\n return { success: false, reason: details };\n};\n\n/** First non-empty line of an error, trimmed to something a TUI line can hold. */\nexport const summarizeFailure = (raw?: string): string | undefined => {\n const line = redactSecrets(raw ?? '')\n .split('\\n')\n .map((l) => l.trim())\n .find((l) => l.length > 0);\n if (!line) return undefined;\n return line.length > 120 ? `${line.slice(0, 117)}...` : line;\n};\n\nexport const toClientResult = (\n name: string,\n result: InstallResult | undefined,\n): McpClientResult => {\n if (!result?.success) {\n return {\n name,\n status: McpClientStatus.Failed,\n detail: summarizeFailure(result?.reason),\n };\n }\n return {\n name,\n status: result.alreadyInstalled\n ? McpClientStatus.Unchanged\n : McpClientStatus.Changed,\n };\n};\n\nexport const isOk = (r: McpClientResult): boolean =>\n r.status !== McpClientStatus.Failed;\n\nexport const namesWithStatus = (\n results: McpClientResult[],\n status: McpClientStatus,\n): string[] => results.filter((r) => r.status === status).map((r) => r.name);\n"],"mappings":";;;;;;;;;;;;;;;AAoDA,MAAa,iBAAiB,QAC5B,IACG,QAAQ,kBAAkB,oBAAoB,CAC9C,QAAQ,4BAA4B,aAAa,CAGjD,QAAQ,mDAAmD,gBAAgB,CAE3E,QAAQ,iCAAiC,aAAa,CACtD,QAAQ,mCAAmC,aAAa,CAExD,QAAQ,4BAA4B,aAAa,CAEjD,QAAQ,2CAA2C,aAAa,CAEhE,QAAQ,6BAA6B,aAAa,CAKlD,QACC,gIACA,iBACD;;;;;;AAOL,MAAa,kBAAkB,QAC7B,IACG,QAAQ,gCAAgC,IAAI,CAC5C,QAAQ,kCAAkC,IAAI;;;;;;;;AAoCnD,MAAa,uBACX,MACA,OACA,UACuB;AACvB,KAAI,CAAC,KAAK,MAAM,CAAE,QAAO,KAAA;AACzB,QAAO,MAAM,MACV,OACE,EAAE,WAAW,KAAA,KACX,UAAU,KAAA,KAAa,EAAE,OAAO,SAAS,MAAM,KAClD,EAAE,MAAM,KAAK,KAAK,IAClB,CAAC,EAAE,QAAQ,KAAK,KAAK,CACxB,EAAE;;;;;;;;;;;AAYL,MAAa,sBAA4C,SAOpC;CACnB,MAAM,EAAE,QAAQ,OAAO,SAAS,OAAO,UAAU;CACjD,MAAM,OAAO,oBAAoB,SAAS,OAAO,MAAM;AACvD,KAAI,MAAM;AAIR,YAAU,cAAc,+BAA+B;GACrD;GACA;GACA;GACA,SAAS,eAAe,QAAQ;GACjC,CAAC;AACF,SAAO;GAAE,SAAS;GAAO,QAAQ;GAAM;;AAEzC,WAAU,iCAAiB,IAAI,MAAM,GAAG,OAAO,GAAG,MAAM,SAAS,EAAE;EACjE;EACA,SAAS,eAAe,QAAQ;EAChC,GAAG;EACJ,CAAC;AACF,QAAO;EAAE,SAAS;EAAO,QAAQ;EAAS;;;AAI5C,MAAa,oBAAoB,QAAqC;CACpE,MAAM,OAAO,cAAc,OAAO,GAAG,CAClC,MAAM,KAAK,CACX,KAAK,MAAM,EAAE,MAAM,CAAC,CACpB,MAAM,MAAM,EAAE,SAAS,EAAE;AAC5B,KAAI,CAAC,KAAM,QAAO,KAAA;AAClB,QAAO,KAAK,SAAS,MAAM,GAAG,KAAK,MAAM,GAAG,IAAI,CAAC,OAAO;;AAG1D,MAAa,kBACX,MACA,WACoB;AACpB,KAAI,CAAC,QAAQ,QACX,QAAO;EACL;EACA,QAAA;EACA,QAAQ,iBAAiB,QAAQ,OAAO;EACzC;AAEH,QAAO;EACL;EACA,QAAQ,OAAO,mBAAA,cAAA;EAGhB;;AAGH,MAAa,QAAQ,MACnB,EAAE,WAAA;AAEJ,MAAa,mBACX,SACA,WACa,QAAQ,QAAQ,MAAM,EAAE,WAAW,OAAO,CAAC,KAAK,MAAM,EAAE,KAAK"}
|
|
1
|
+
{"version":3,"file":"results-BE1ctlKP.js","names":[],"sources":["../src/shared/mcp-clients/results.ts"],"sourcesContent":["import { analytics } from '@utils/analytics';\n\n/**\n * Per-client outcome of an MCP server / plugin install or removal.\n *\n * \"Nothing to do\" is a first-class outcome rather than a silent no-op:\n * re-running `mcp add` on a machine that's already set up is the common case,\n * and collapsing it into an empty result set is what made the flow report\n * \"Installation skipped.\" with no explanation. Failures carry a short reason\n * for the same reason — an empty list tells the user nothing.\n *\n * The names are action-neutral because both `mcp add` and `mcp remove` report\n * through them: `Changed` is \"installed\" or \"removed\" depending on the flow.\n */\n\nexport enum McpClientStatus {\n /** We made the change — wrote the config, installed or removed the server. */\n Changed = 'changed',\n /** Already in the requested state; nothing was touched. */\n Unchanged = 'unchanged',\n Failed = 'failed',\n}\n\nexport interface McpClientResult {\n name: string;\n status: McpClientStatus;\n /** Short, user-facing explanation. Set for failures. */\n detail?: string;\n}\n\n/** Result shape every client's addServer/removeServer/installPlugin returns. */\nexport interface InstallResult {\n success: boolean;\n /** Already in the requested state — nothing was written or removed. */\n alreadyInstalled?: boolean;\n /** Raw failure text from the underlying CLI or filesystem error. */\n reason?: string;\n}\n\n/**\n * Failure text comes from CLIs we invoke with the user's personal API key on the\n * command line (`--header \"Authorization: Bearer phx_...\"`), so it can carry the\n * key into the log file, the TUI and exception reports. Mask it first.\n *\n * The plugin stages clone git repositories and read the user's own config, so\n * the text reaching here is no longer only ours: a failing clone echoes the\n * remote URL with any credentials embedded in it, and a config error can quote\n * a foreign provider's key. Orchestrator task remarks reach analytics through\n * here too, from a step that collects database and API credentials. Each\n * pattern below is a shape one of these sources can carry, not a guess at\n * every possible secret.\n */\nexport const redactSecrets = (raw: string): string =>\n raw\n .replace(/Bearer\\s+\\S+/gi, 'Bearer [redacted]')\n .replace(/ph[xspc]_[A-Za-z0-9_-]+/g, '[redacted]')\n // `https://user:token@github.com/...` — git prints the whole remote back;\n // `postgres://user:password@host/db` is the same shape for a database.\n .replace(/(\\b[a-z][a-z0-9+.-]*:\\/\\/)[^/\\s:@]+:[^/\\s@]+@/gi, '$1[redacted]@')\n // GitHub tokens, which a clone failure quotes verbatim.\n .replace(/\\bgh[pousr]_[A-Za-z0-9]{16,}/g, '[redacted]')\n .replace(/\\bgithub_pat_[A-Za-z0-9_]{20,}/g, '[redacted]')\n // OpenAI keys: `OPENAI_API_KEY` is already a failure codex reports on.\n .replace(/\\bsk-[A-Za-z0-9_-]{16,}/g, '[redacted]')\n // Stripe secret and restricted keys, which the warehouse step collects.\n .replace(/\\b[rs]k_(?:live|test)_[A-Za-z0-9]{16,}/g, '[redacted]')\n // Google keys, which a config error quotes the same way.\n .replace(/\\bAIza[A-Za-z0-9_-]{16,}/g, '[redacted]')\n // The catch-all for providers we have no shape for: a config error prints\n // the offending line, and the variable's own name says it holds a secret.\n // The name is kept — `OPENAI_API_KEY` is wording an expected-failure hint\n // matches on, and masking it would turn that hint back into an exception.\n .replace(\n /\\b([A-Za-z0-9_]*(?:KEY|TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIALS?)[A-Za-z0-9_]*)(\\s*[=:]\\s*)[\"']?[A-Za-z0-9_\\-./+]{8,}[\"']?/gi,\n '$1$2[redacted]',\n );\n\n/**\n * Replace the user's home directory with `~`. Error tracking fingerprints on\n * the message, so a path that differs per user splits one root cause into one\n * issue per user — and the username itself never needs to leave the machine.\n */\nexport const scrubHomePaths = (raw: string): string =>\n raw\n .replace(/\\/(?:Users|home)\\/[^/\\s'\"]+/g, '~')\n .replace(/[A-Za-z]:\\\\Users\\\\[^\\\\\\s'\"]+/gi, '~');\n\n/**\n * A failure that lives entirely in the user's environment: a CLI too old, a\n * broken install, an unreadable config. We can't fix these, so we hand back a\n * hint instead of filing an issue nobody can action.\n *\n * Silencing a failure is the expensive direction: a pattern that matches too\n * much retires a real bug of ours into a hint nobody reads. So an entry says\n * where it applies (`stages`) and what disqualifies it (`unless`), and anything\n * it cannot claim confidently goes back to being reported.\n */\nexport interface ExpectedFailure {\n match: RegExp;\n /**\n * Stages this hint is allowed to fire on. Omitted means any stage. Wording\n * that means one thing during `plugin install` can mean something else during\n * `mcp add`, and a hint naming the wrong command is worse than none.\n */\n stages?: string[];\n /**\n * Wording that disqualifies the match even when `match` hits — the tell that\n * the failure is ours, not the environment's.\n */\n unless?: RegExp;\n /** One short line, shown to the user as the failure detail. */\n hint: string;\n}\n\n/**\n * The first matching hint, or undefined when the failure is worth reporting.\n *\n * `stage` is the caller's name for what it was doing. Passing it lets an entry\n * narrow itself; omitting it only considers entries that claim every stage,\n * because an unscoped call cannot honour a scoped entry.\n */\nexport const expectedFailureHint = (\n text: string,\n table: ExpectedFailure[],\n stage?: string,\n): string | undefined => {\n if (!text.trim()) return undefined;\n return table.find(\n (f) =>\n (f.stages === undefined ||\n (stage !== undefined && f.stages.includes(stage))) &&\n f.match.test(text) &&\n !f.unless?.test(text),\n )?.hint;\n};\n\n/**\n * Turn a failed CLI call into a result: an expected local failure becomes a\n * hint, anything else is reported under a constant message so one root cause\n * stays one issue, with the varying detail in properties.\n *\n * Shared by every client so the hinted-event shape cannot drift between them.\n * `Stage` is the caller's union: a hint scoped to a stage nobody passes is\n * silently dead.\n */\nexport const reportSpawnFailure = <Stage extends string>(opts: {\n client: string;\n stage: Stage;\n details: string;\n table: ExpectedFailure[];\n /** Extra exception properties, e.g. a failure that travelled beside this one. */\n extra?: Record<string, unknown>;\n}): InstallResult => {\n const { client, stage, details, table, extra } = opts;\n const hint = expectedFailureHint(details, table, stage);\n if (hint) {\n // Hinting takes a failure out of error tracking, so without this the only\n // evidence a pattern has started over-matching is that our exception count\n // fell, which reads as the fix working. An event keeps the count.\n analytics.wizardCapture('mcp expected failure hinted', {\n client,\n stage,\n hint,\n details: scrubHomePaths(details),\n });\n return { success: false, reason: hint };\n }\n analytics.captureException(new Error(`${client} ${stage} failed`), {\n stage,\n details: scrubHomePaths(details),\n ...extra,\n });\n return { success: false, reason: details };\n};\n\n/** First non-empty line of an error, trimmed to something a TUI line can hold. */\nexport const summarizeFailure = (raw?: string): string | undefined => {\n const line = redactSecrets(raw ?? '')\n .split('\\n')\n .map((l) => l.trim())\n .find((l) => l.length > 0);\n if (!line) return undefined;\n return line.length > 120 ? `${line.slice(0, 117)}...` : line;\n};\n\nexport const toClientResult = (\n name: string,\n result: InstallResult | undefined,\n): McpClientResult => {\n if (!result?.success) {\n return {\n name,\n status: McpClientStatus.Failed,\n detail: summarizeFailure(result?.reason),\n };\n }\n return {\n name,\n status: result.alreadyInstalled\n ? McpClientStatus.Unchanged\n : McpClientStatus.Changed,\n };\n};\n\nexport const isOk = (r: McpClientResult): boolean =>\n r.status !== McpClientStatus.Failed;\n\nexport const namesWithStatus = (\n results: McpClientResult[],\n status: McpClientStatus,\n): string[] => results.filter((r) => r.status === status).map((r) => r.name);\n"],"mappings":";;;;;;;;;;;;;;;AAoDA,MAAa,iBAAiB,QAC5B,IACG,QAAQ,kBAAkB,oBAAoB,CAC9C,QAAQ,4BAA4B,aAAa,CAGjD,QAAQ,mDAAmD,gBAAgB,CAE3E,QAAQ,iCAAiC,aAAa,CACtD,QAAQ,mCAAmC,aAAa,CAExD,QAAQ,4BAA4B,aAAa,CAEjD,QAAQ,2CAA2C,aAAa,CAEhE,QAAQ,6BAA6B,aAAa,CAKlD,QACC,gIACA,iBACD;;;;;;AAOL,MAAa,kBAAkB,QAC7B,IACG,QAAQ,gCAAgC,IAAI,CAC5C,QAAQ,kCAAkC,IAAI;;;;;;;;AAoCnD,MAAa,uBACX,MACA,OACA,UACuB;AACvB,KAAI,CAAC,KAAK,MAAM,CAAE,QAAO,KAAA;AACzB,QAAO,MAAM,MACV,OACE,EAAE,WAAW,KAAA,KACX,UAAU,KAAA,KAAa,EAAE,OAAO,SAAS,MAAM,KAClD,EAAE,MAAM,KAAK,KAAK,IAClB,CAAC,EAAE,QAAQ,KAAK,KAAK,CACxB,EAAE;;;;;;;;;;;AAYL,MAAa,sBAA4C,SAOpC;CACnB,MAAM,EAAE,QAAQ,OAAO,SAAS,OAAO,UAAU;CACjD,MAAM,OAAO,oBAAoB,SAAS,OAAO,MAAM;AACvD,KAAI,MAAM;AAIR,YAAU,cAAc,+BAA+B;GACrD;GACA;GACA;GACA,SAAS,eAAe,QAAQ;GACjC,CAAC;AACF,SAAO;GAAE,SAAS;GAAO,QAAQ;GAAM;;AAEzC,WAAU,iCAAiB,IAAI,MAAM,GAAG,OAAO,GAAG,MAAM,SAAS,EAAE;EACjE;EACA,SAAS,eAAe,QAAQ;EAChC,GAAG;EACJ,CAAC;AACF,QAAO;EAAE,SAAS;EAAO,QAAQ;EAAS;;;AAI5C,MAAa,oBAAoB,QAAqC;CACpE,MAAM,OAAO,cAAc,OAAO,GAAG,CAClC,MAAM,KAAK,CACX,KAAK,MAAM,EAAE,MAAM,CAAC,CACpB,MAAM,MAAM,EAAE,SAAS,EAAE;AAC5B,KAAI,CAAC,KAAM,QAAO,KAAA;AAClB,QAAO,KAAK,SAAS,MAAM,GAAG,KAAK,MAAM,GAAG,IAAI,CAAC,OAAO;;AAG1D,MAAa,kBACX,MACA,WACoB;AACpB,KAAI,CAAC,QAAQ,QACX,QAAO;EACL;EACA,QAAA;EACA,QAAQ,iBAAiB,QAAQ,OAAO;EACzC;AAEH,QAAO;EACL;EACA,QAAQ,OAAO,mBAAA,cAAA;EAGhB;;AAGH,MAAa,QAAQ,MACnB,EAAE,WAAA;AAEJ,MAAa,mBACX,SACA,WACa,QAAQ,QAAQ,MAAM,EAAE,WAAW,OAAO,CAAC,KAAK,MAAM,EAAE,KAAK"}
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import { a as initLogFile, f as VERSION, o as logToFile } from "./debug-
|
|
2
|
-
import { n as analytics } from "./analytics-
|
|
3
|
-
import { A as getProgramConfig, a as PostHogDestination, c as buildSession, i as createWizardRunSync, o as createFileDestination, ot as getAuditChecks, r as TaskStreamPush, s as storeInteraction, u as runProgram } from "./programs-
|
|
1
|
+
import { a as initLogFile, f as VERSION, o as logToFile } from "./debug-BWrfAT_d.js";
|
|
2
|
+
import { n as analytics } from "./analytics-J1cjIekQ.js";
|
|
3
|
+
import { A as getProgramConfig, a as PostHogDestination, c as buildSession, i as createWizardRunSync, o as createFileDestination, ot as getAuditChecks, r as TaskStreamPush, s as storeInteraction, u as runProgram } from "./programs-AZStBYjR.js";
|
|
4
4
|
import { t as classifyRunFailure } from "./errors-BXlWujEw.js";
|
|
5
5
|
import { n as printAbortOutro } from "./console-log-BMhBoX-o.js";
|
|
6
|
-
import { m as runCleanups } from "./agent-interface-
|
|
7
|
-
import { C as loadWizardFlags, i as ProgramAbort } from "./posthog-integration-
|
|
8
|
-
import { c as flushAnalytics } from "./tools-
|
|
9
|
-
import { n as startHostExit, r as wizardAbort, t as registerShutdown } from "./wizard-abort-
|
|
10
|
-
import { O as displayProgress, _ as getTuiTool, m as isRunFailure, x as abortOnScreens } from "./terminal-
|
|
11
|
-
import { a as startTUI, n as localServicesError, o as oauthCredentials, r as reportFatal, t as driveTuiTool } from "./run-tool-
|
|
6
|
+
import { m as runCleanups } from "./agent-interface-C2yoWWW_.js";
|
|
7
|
+
import { C as loadWizardFlags, i as ProgramAbort } from "./posthog-integration-yK9EktZn.js";
|
|
8
|
+
import { c as flushAnalytics } from "./tools-Blf-Yjru.js";
|
|
9
|
+
import { n as startHostExit, r as wizardAbort, t as registerShutdown } from "./wizard-abort-g6PYcdlP.js";
|
|
10
|
+
import { O as displayProgress, _ as getTuiTool, m as isRunFailure, x as abortOnScreens } from "./terminal-CmCNn0IG.js";
|
|
11
|
+
import { a as startTUI, n as localServicesError, o as oauthCredentials, r as reportFatal, t as driveTuiTool } from "./run-tool-DAt35gkq.js";
|
|
12
12
|
import { join } from "node:path";
|
|
13
13
|
//#region src/tui/workflow.ts
|
|
14
14
|
function tuiWorkflow(store, programId) {
|
|
@@ -261,4 +261,4 @@ async function runProgramOnScreens(config, store, { credentials, signal }) {
|
|
|
261
261
|
//#endregion
|
|
262
262
|
export { runTui };
|
|
263
263
|
|
|
264
|
-
//# sourceMappingURL=run-
|
|
264
|
+
//# sourceMappingURL=run-Cmn1xaWK.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"run-W03b6jNM.js","names":[],"sources":["../src/tui/workflow.ts","../src/tui/run.ts"],"sourcesContent":["/** The TUI's answers to the steps `runProgram` waits on: its screens, gates and overlays. */\nimport { RunOutcome } from '@programs';\nimport type { ProgramId, ProgramWorkflowConnector } from '@programs/types';\nimport type { WizardStore } from './store.js';\n\nexport function tuiWorkflow(\n store: WizardStore,\n programId: ProgramId,\n): ProgramWorkflowConnector {\n return {\n async confirmStep(step) {\n switch (step.kind) {\n case 'ai-approval':\n // AiOptInRequiredScreen holds here, before any source leaves the machine.\n await store.getGate('ai-opt-in');\n return true;\n case 'service-outage':\n // The health-check screen shows the outage; the run stops.\n store.setReadinessResult(step.readiness);\n return false;\n case 'settings-conflict':\n await store.showSettingsOverride(step.conflicts, step.fix);\n return true;\n case 'run':\n // Each screen before the run's step settles first; a hidden step doesn't run.\n return store.reachStep(step.stepId);\n }\n },\n finishStep(step, result) {\n // Only a composed sub-run that succeeded completes its step; a failed one keeps its tasks for the outro and the stream. The program's run step follows its phase.\n if (\n step.programId !== programId &&\n result.outcome === RunOutcome.Success\n ) {\n store.completeRunStep(step.stepId);\n }\n },\n };\n}\n","/**\n * The TUI host: the full-screen wizard. It renders its screens over its own\n * store, settles the intro, and calls `runProgram` once with the OAuth login,\n * the WizardAsk screen as the answerer and its screens as the workflow. An\n * intro that hands off to a tool drives the tool's screens instead. The CLI\n * builds the launch values, owns the signals and applies the exit code.\n */\nimport { join } from 'node:path';\nimport {\n buildSession,\n createFileDestination,\n createWizardRunSync,\n getAuditChecks,\n getProgramConfig,\n loadWizardFlags,\n PostHogDestination,\n ProgramAbort,\n RunOutcome,\n runProgram,\n storeInteraction,\n TaskStreamPush,\n} from '@programs';\nimport type { CredentialsProvider, ProgramConfig } from '@programs/types';\nimport { classifyRunFailure } from '@shared/errors';\nimport { OutroKind } from '@shared/outro';\nimport { RunPhase } from '@shared/run-state';\nimport { VERSION } from '@shared/version';\nimport { analytics } from '@utils/analytics';\nimport { runCleanups } from '@utils/cleanup';\nimport { initLogFile, logToFile } from '@utils/debug';\nimport { flushAnalytics } from '@utils/flush-analytics';\nimport {\n registerShutdown,\n startHostExit,\n wizardAbort,\n} from '@host/wizard-abort';\nimport { printAbortOutro } from '@shared/console-log';\nimport { getTuiTool } from '@tui/tools/index';\nimport { abortOnScreens } from './abort.js';\nimport { displayProgress } from './agent-progress.js';\nimport { oauthCredentials } from './auth/login.js';\nimport { isRunFailure } from './mint-failure.js';\nimport { driveTuiTool, localServicesError, reportFatal } from './run-tool.js';\nimport { startTUI } from './start-tui.js';\nimport type { WizardStore } from './store.js';\nimport type { TuiLaunch } from './launch.js';\nimport { tuiWorkflow } from './workflow.js';\n\n/** How long a screen's exit request waits for analytics before it exits anyway. */\nconst EXIT_REPORT_BUDGET_MS = 2000;\n\n/**\n * Run `config` in the TUI. Resolves with the exit code: the run's, a screen's\n * exit request, a decided failure's through `wizardAbort`, or 130 or 143 on a signal.\n */\nexport async function runTui(\n config: ProgramConfig,\n launch: TuiLaunch,\n): Promise<number> {\n const exit = startHostExit();\n let tui: ReturnType<typeof startTUI> | null = null;\n let stream: TaskStreamPush | null = null;\n let unregisterShutdown: (() => void) | undefined;\n // A signal cancels the run and any pending question before the exit.\n const runAbort = new AbortController();\n let exitInProgress = false;\n let signalled = false;\n // A tool the intro handed off to ends the run once its analytics flush.\n let handedOff = false;\n\n // Flush a terminal-phase push so the web app sees the run ended in error\n // rather than hanging on the last \"running\" snapshot. Registered before the\n // stream exists: Ctrl-C on the intro must still restore the terminal and run\n // the cleanups, and there is no run to report yet.\n const onSignal = (signal: 'SIGINT' | 'SIGTERM'): void => {\n if (signalled || exitInProgress || exit.ended) return;\n signalled = true;\n runAbort.abort();\n logToFile('[run-wizard] signal received, flushing task stream');\n // Settings restore is sync fs work, so it runs before a flush that may time out.\n runCleanups();\n if (tui?.store.session.runPhase === RunPhase.Running) {\n tui.store.setRunPhase(RunPhase.Error);\n }\n void Promise.all([\n stream?.shutdown(2000, 'cancelled'),\n analytics.shutdown('cancelled'),\n ])\n .catch(() => logToFile('[run-wizard] cancellation shutdown failed'))\n .finally(() => {\n unregisterShutdown?.();\n try {\n tui?.unmount();\n } catch {\n // terminal may already be torn down\n }\n exit.end(signal === 'SIGTERM' ? 143 : 130);\n });\n };\n const onAbort = () =>\n onSignal(launch.signal.reason === 'SIGTERM' ? 'SIGTERM' : 'SIGINT');\n // A signal that landed before this host subscribed ends the run too.\n if (launch.signal.aborted) onAbort();\n else launch.signal.addEventListener('abort', onAbort, { once: true });\n\n // Before the TUI mounts: once Ink owns the alt screen, anything written\n // to it is wiped on unmount (see the catch block below), so an abort here\n // would leave the user on a loading screen with no message.\n const localError = await localServicesError(launch.session.baseUrl);\n if (signalled) return exit.exited;\n if (localError) {\n wizardAbort(\n { present: printAbortOutro, exit },\n { message: localError },\n ).catch((error: unknown) => exit.fail(error));\n return exit.exited;\n }\n\n const main = async (): Promise<void> => {\n // Ink handles Ctrl-C itself in raw mode; it arrives here as an interrupt.\n tui = startTUI(VERSION, config.id, () => onSignal('SIGINT'));\n const activeTui = tui;\n const { store } = activeTui;\n // A screen's exit request unmounts, reports the run's end within a bounded wait, then exits with no stream shutdown.\n store.subscribe(() => {\n const code = store.exitRequest;\n if (code === null || handedOff || exitInProgress || signalled) return;\n exitInProgress = true;\n launch.signal.removeEventListener('abort', onAbort);\n activeTui.unmount();\n const report = async (): Promise<void> => {\n try {\n await analytics.shutdown(code === 0 ? 'cancelled' : 'error');\n } catch {\n logToFile('[run-wizard] exit request shutdown failed');\n }\n await flushAnalytics();\n };\n void Promise.race([\n report(),\n new Promise((resolve) => setTimeout(resolve, EXIT_REPORT_BUDGET_MS)),\n ]).then(() => exit.end(code));\n });\n\n const session = buildSession(launch.session);\n if (launch.skillId) {\n session.skillId = launch.skillId;\n } else if (config.skillId) {\n session.skillId = config.skillId;\n }\n store.launch(session, launch.session, config.id);\n\n const credentials = launch.credentials ?? oauthCredentials(store);\n launch.onStore?.(store);\n\n // Detection, then the intro, where the user may switch program.\n for (;;) {\n try {\n await store.runReadyHooks();\n } catch (error) {\n if (!(error instanceof ProgramAbort)) throw error;\n await abortOnScreens(store, {\n code: error.code,\n message: error.message,\n outroData: error.outroData,\n });\n }\n await store.getGate('intro');\n const active = store.router.activeProgram;\n if (active === config.id) break;\n const tool = getTuiTool(active);\n if (tool) {\n handedOff = true;\n const code = await driveTuiTool(store, tool, {\n toolId: active,\n signal: runAbort.signal,\n });\n if (signalled || exit.ended) return;\n exitInProgress = true;\n launch.signal.removeEventListener('abort', onAbort);\n activeTui.unmount();\n await flushAnalytics();\n exit.end(code);\n return;\n }\n config = getProgramConfig(active);\n }\n\n // After the switch loop, not before: the stream bakes its program id,\n // session id, and event-plan path in at construction, so a stream built\n // for the launch program would report the whole run under a program the\n // user left on the intro screen. Nothing before this point produces a\n // task to push.\n // Consent gates the push, not the dump: `--no-telemetry` still logs.\n const fileDestination = createFileDestination(launch.taskStreamLog);\n const destinations = [\n ...(session.noTelemetry\n ? []\n : [\n new PostHogDestination({\n getCredentials: () => store.session.credentials,\n onError: (err) => logToFile('[task-stream-push]', err.message),\n }),\n ]),\n ...(fileDestination ? [fileDestination] : []),\n ];\n const programConfig = config;\n const activeStream = new TaskStreamPush({\n store: store.sessions,\n getFlags: () => analytics.getCachedWizardFlags(),\n programId: programConfig.streamWorkflowId ?? programConfig.id,\n runSync: createWizardRunSync({\n mode: 'local',\n programId: programConfig.id,\n assignedId: launch.runId,\n noTelemetry: session.noTelemetry,\n getSession: () => store.session,\n }),\n destinations,\n eventPlanPath: programConfig.eventPlanFile\n ? join(session.installDir, programConfig.eventPlanFile)\n : undefined,\n auditChecks: programConfig.auditLedgerFile\n ? () => getAuditChecks(store.session)\n : undefined,\n enabled: destinations.length > 0,\n });\n stream = activeStream;\n activeStream.attach();\n unregisterShutdown = registerShutdown((outcome) => {\n if (store.session.runPhase === RunPhase.Running) {\n store.setRunPhase(RunPhase.Error);\n }\n return activeStream.shutdown(2000, outcome);\n });\n\n await store.getGate('integration-check');\n await store.getGate('health-check');\n\n try {\n await runProgramOnScreens(programConfig, store, {\n credentials,\n signal: runAbort.signal,\n });\n } catch (error) {\n // The run threw before its own error handling rendered an outro.\n // Show the handoff screen and let the user's agent take over.\n const failure = classifyRunFailure(error);\n logToFile('[run-wizard] run failed, handing off:', error);\n runCleanups();\n analytics.captureException(\n error instanceof Error ? error : new Error(String(error)),\n { error_code: failure.code },\n );\n store.showOutroError({\n kind: OutroKind.Error,\n errorCode: failure.code,\n message: failure.message,\n });\n }\n\n if (signalled) return;\n const runFailed = isRunFailure(store.session);\n await activeStream.finishRun(runFailed ? 'failed' : 'completed');\n await store.waitUntil((s) => s.mintHandoff === 'exit' || s.skillsComplete);\n // A screen already ended the run (KeepSkills after a success): start no flush it would cut off.\n if (exit.ended || exitInProgress) return;\n\n exitInProgress = true;\n await activeStream.shutdown(2000);\n unregisterShutdown?.();\n launch.signal.removeEventListener('abort', onAbort);\n if (runFailed) await analytics.shutdown('error');\n activeTui.unmount();\n exit.end(runFailed ? 1 : 0);\n };\n\n main().catch(async (err: unknown) => {\n if (signalled || exit.ended) return;\n // File-log first — the cleanup below can throw or exit.\n logToFile('[run-wizard] FATAL:', err);\n // Run cleanups before anything async so settings are restored even if\n // the stream shutdown hangs.\n runCleanups();\n // The task-stream debounce timer keeps the event loop alive, so\n // we have to drain it before exiting on the error path.\n exitInProgress = true;\n launch.signal.removeEventListener('abort', onAbort);\n try {\n await stream?.shutdown(2000, 'failed');\n } catch {\n // ignore\n }\n unregisterShutdown?.();\n try {\n tui?.unmount();\n } catch {\n // ignore\n }\n reportFatal(err);\n exit.end(1);\n });\n\n return exit.exited;\n}\n\n/**\n * The program's run, answered by the screens: `credentials` logs in, the\n * WizardAsk screen answers, and the flow's screens settle each step. A failure\n * shows its outro and ends the run through `wizardAbort`.\n */\nexport async function runProgramOnScreens(\n config: ProgramConfig,\n store: WizardStore,\n {\n credentials,\n signal,\n }: { credentials: CredentialsProvider; signal?: AbortSignal },\n): Promise<void> {\n initLogFile();\n logToFile(`[agent-runner] START ${config.id} build=${analytics.build}`);\n\n // runProgram turns a throwing capability into a failed run; the TUI shows\n // the handoff for those, so the throw is kept and rethrown.\n let capabilityFailure: { error: unknown } | undefined;\n const keepFailure = <T>(work: Promise<T>): Promise<T> =>\n work.catch((error: unknown) => {\n capabilityFailure ??= { error };\n throw error;\n });\n const result = await runProgram(\n config.id,\n { store: store.sessions, config },\n {\n credentials: {\n resolve: (programId, context) =>\n keepFailure(credentials.resolve(programId, context)),\n },\n interaction: storeInteraction(store.sessions),\n workflow: tuiWorkflow(store, config.id),\n onProgress: displayProgress(store),\n featureFlags: () => keepFailure(loadWizardFlags()),\n signal,\n },\n );\n if (capabilityFailure) throw capabilityFailure.error;\n // A signal cancelled the run; the handler that aborted it settles.\n if (signal?.aborted) return;\n\n if (result.outcome === RunOutcome.Crashed) throw result.failure?.error;\n if (result.outcome !== RunOutcome.Success) {\n if (result.failure?.authErrorDetail) {\n store.showAuthError(result.failure.authErrorDetail);\n }\n // The terminal status follows how the run ended, not whether an Error came back.\n await abortOnScreens(store, {\n ...result.failure,\n status: result.outcome === RunOutcome.Aborted ? 'cancelled' : 'error',\n });\n return;\n }\n // The run already succeeded: a failed flush is logged, never the outcome.\n try {\n await analytics.shutdown('success');\n } catch (error) {\n logToFile('[agent-runner] analytics shutdown failed:', error);\n }\n}\n"],"mappings":";;;;;;;;;;;;;AAKA,SAAgB,YACd,OACA,WAC0B;AAC1B,QAAO;EACL,MAAM,YAAY,MAAM;AACtB,WAAQ,KAAK,MAAb;IACE,KAAK;AAEH,WAAM,MAAM,QAAQ,YAAY;AAChC,YAAO;IACT,KAAK;AAEH,WAAM,mBAAmB,KAAK,UAAU;AACxC,YAAO;IACT,KAAK;AACH,WAAM,MAAM,qBAAqB,KAAK,WAAW,KAAK,IAAI;AAC1D,YAAO;IACT,KAAK,MAEH,QAAO,MAAM,UAAU,KAAK,OAAO;;;EAGzC,WAAW,MAAM,QAAQ;AAEvB,OACE,KAAK,cAAc,aACnB,OAAO,YAAA,UAEP,OAAM,gBAAgB,KAAK,OAAO;;EAGvC;;;;;;;;;;;;ACYH,MAAM,wBAAwB;;;;;AAM9B,eAAsB,OACpB,QACA,QACiB;CACjB,MAAM,OAAO,eAAe;CAC5B,IAAI,MAA0C;CAC9C,IAAI,SAAgC;CACpC,IAAI;CAEJ,MAAM,WAAW,IAAI,iBAAiB;CACtC,IAAI,iBAAiB;CACrB,IAAI,YAAY;CAEhB,IAAI,YAAY;CAMhB,MAAM,YAAY,WAAuC;AACvD,MAAI,aAAa,kBAAkB,KAAK,MAAO;AAC/C,cAAY;AACZ,WAAS,OAAO;AAChB,YAAU,qDAAqD;AAE/D,eAAa;AACb,MAAI,KAAK,MAAM,QAAQ,aAAA,UACrB,KAAI,MAAM,YAAA,QAA2B;AAElC,UAAQ,IAAI,CACf,QAAQ,SAAS,KAAM,YAAY,EACnC,UAAU,SAAS,YAAY,CAChC,CAAC,CACC,YAAY,UAAU,4CAA4C,CAAC,CACnE,cAAc;AACb,yBAAsB;AACtB,OAAI;AACF,SAAK,SAAS;WACR;AAGR,QAAK,IAAI,WAAW,YAAY,MAAM,IAAI;IAC1C;;CAEN,MAAM,gBACJ,SAAS,OAAO,OAAO,WAAW,YAAY,YAAY,SAAS;AAErE,KAAI,OAAO,OAAO,QAAS,UAAS;KAC/B,QAAO,OAAO,iBAAiB,SAAS,SAAS,EAAE,MAAM,MAAM,CAAC;CAKrE,MAAM,aAAa,MAAM,mBAAmB,OAAO,QAAQ,QAAQ;AACnE,KAAI,UAAW,QAAO,KAAK;AAC3B,KAAI,YAAY;AACd,cACE;GAAE,SAAS;GAAiB;GAAM,EAClC,EAAE,SAAS,YAAY,CACxB,CAAC,OAAO,UAAmB,KAAK,KAAK,MAAM,CAAC;AAC7C,SAAO,KAAK;;CAGd,MAAM,OAAO,YAA2B;AAEtC,QAAM,SAAS,SAAS,OAAO,UAAU,SAAS,SAAS,CAAC;EAC5D,MAAM,YAAY;EAClB,MAAM,EAAE,UAAU;AAElB,QAAM,gBAAgB;GACpB,MAAM,OAAO,MAAM;AACnB,OAAI,SAAS,QAAQ,aAAa,kBAAkB,UAAW;AAC/D,oBAAiB;AACjB,UAAO,OAAO,oBAAoB,SAAS,QAAQ;AACnD,aAAU,SAAS;GACnB,MAAM,SAAS,YAA2B;AACxC,QAAI;AACF,WAAM,UAAU,SAAS,SAAS,IAAI,cAAc,QAAQ;YACtD;AACN,eAAU,4CAA4C;;AAExD,UAAM,gBAAgB;;AAEnB,WAAQ,KAAK,CAChB,QAAQ,EACR,IAAI,SAAS,YAAY,WAAW,SAAS,sBAAsB,CAAC,CACrE,CAAC,CAAC,WAAW,KAAK,IAAI,KAAK,CAAC;IAC7B;EAEF,MAAM,UAAU,aAAa,OAAO,QAAQ;AAC5C,MAAI,OAAO,QACT,SAAQ,UAAU,OAAO;WAChB,OAAO,QAChB,SAAQ,UAAU,OAAO;AAE3B,QAAM,OAAO,SAAS,OAAO,SAAS,OAAO,GAAG;EAEhD,MAAM,cAAc,OAAO,eAAe,iBAAiB,MAAM;AACjE,SAAO,UAAU,MAAM;AAGvB,WAAS;AACP,OAAI;AACF,UAAM,MAAM,eAAe;YACpB,OAAO;AACd,QAAI,EAAE,iBAAiB,cAAe,OAAM;AAC5C,UAAM,eAAe,OAAO;KAC1B,MAAM,MAAM;KACZ,SAAS,MAAM;KACf,WAAW,MAAM;KAClB,CAAC;;AAEJ,SAAM,MAAM,QAAQ,QAAQ;GAC5B,MAAM,SAAS,MAAM,OAAO;AAC5B,OAAI,WAAW,OAAO,GAAI;GAC1B,MAAM,OAAO,WAAW,OAAO;AAC/B,OAAI,MAAM;AACR,gBAAY;IACZ,MAAM,OAAO,MAAM,aAAa,OAAO,MAAM;KAC3C,QAAQ;KACR,QAAQ,SAAS;KAClB,CAAC;AACF,QAAI,aAAa,KAAK,MAAO;AAC7B,qBAAiB;AACjB,WAAO,OAAO,oBAAoB,SAAS,QAAQ;AACnD,cAAU,SAAS;AACnB,UAAM,gBAAgB;AACtB,SAAK,IAAI,KAAK;AACd;;AAEF,YAAS,iBAAiB,OAAO;;EASnC,MAAM,kBAAkB,sBAAsB,OAAO,cAAc;EACnE,MAAM,eAAe,CACnB,GAAI,QAAQ,cACR,EAAE,GACF,CACE,IAAI,mBAAmB;GACrB,sBAAsB,MAAM,QAAQ;GACpC,UAAU,QAAQ,UAAU,sBAAsB,IAAI,QAAQ;GAC/D,CAAC,CACH,EACL,GAAI,kBAAkB,CAAC,gBAAgB,GAAG,EAAE,CAC7C;EACD,MAAM,gBAAgB;EACtB,MAAM,eAAe,IAAI,eAAe;GACtC,OAAO,MAAM;GACb,gBAAgB,UAAU,sBAAsB;GAChD,WAAW,cAAc,oBAAoB,cAAc;GAC3D,SAAS,oBAAoB;IAC3B,MAAM;IACN,WAAW,cAAc;IACzB,YAAY,OAAO;IACnB,aAAa,QAAQ;IACrB,kBAAkB,MAAM;IACzB,CAAC;GACF;GACA,eAAe,cAAc,gBACzB,KAAK,QAAQ,YAAY,cAAc,cAAc,GACrD,KAAA;GACJ,aAAa,cAAc,wBACjB,eAAe,MAAM,QAAQ,GACnC,KAAA;GACJ,SAAS,aAAa,SAAS;GAChC,CAAC;AACF,WAAS;AACT,eAAa,QAAQ;AACrB,uBAAqB,kBAAkB,YAAY;AACjD,OAAI,MAAM,QAAQ,aAAA,UAChB,OAAM,YAAA,QAA2B;AAEnC,UAAO,aAAa,SAAS,KAAM,QAAQ;IAC3C;AAEF,QAAM,MAAM,QAAQ,oBAAoB;AACxC,QAAM,MAAM,QAAQ,eAAe;AAEnC,MAAI;AACF,SAAM,oBAAoB,eAAe,OAAO;IAC9C;IACA,QAAQ,SAAS;IAClB,CAAC;WACK,OAAO;GAGd,MAAM,UAAU,mBAAmB,MAAM;AACzC,aAAU,yCAAyC,MAAM;AACzD,gBAAa;AACb,aAAU,iBACR,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,OAAO,MAAM,CAAC,EACzD,EAAE,YAAY,QAAQ,MAAM,CAC7B;AACD,SAAM,eAAe;IACnB,MAAA;IACA,WAAW,QAAQ;IACnB,SAAS,QAAQ;IAClB,CAAC;;AAGJ,MAAI,UAAW;EACf,MAAM,YAAY,aAAa,MAAM,QAAQ;AAC7C,QAAM,aAAa,UAAU,YAAY,WAAW,YAAY;AAChE,QAAM,MAAM,WAAW,MAAM,EAAE,gBAAgB,UAAU,EAAE,eAAe;AAE1E,MAAI,KAAK,SAAS,eAAgB;AAElC,mBAAiB;AACjB,QAAM,aAAa,SAAS,IAAK;AACjC,wBAAsB;AACtB,SAAO,OAAO,oBAAoB,SAAS,QAAQ;AACnD,MAAI,UAAW,OAAM,UAAU,SAAS,QAAQ;AAChD,YAAU,SAAS;AACnB,OAAK,IAAI,YAAY,IAAI,EAAE;;AAG7B,OAAM,CAAC,MAAM,OAAO,QAAiB;AACnC,MAAI,aAAa,KAAK,MAAO;AAE7B,YAAU,uBAAuB,IAAI;AAGrC,eAAa;AAGb,mBAAiB;AACjB,SAAO,OAAO,oBAAoB,SAAS,QAAQ;AACnD,MAAI;AACF,SAAM,QAAQ,SAAS,KAAM,SAAS;UAChC;AAGR,wBAAsB;AACtB,MAAI;AACF,QAAK,SAAS;UACR;AAGR,cAAY,IAAI;AAChB,OAAK,IAAI,EAAE;GACX;AAEF,QAAO,KAAK;;;;;;;AAQd,eAAsB,oBACpB,QACA,OACA,EACE,aACA,UAEa;AACf,cAAa;AACb,WAAU,wBAAwB,OAAO,GAAG,SAAS,UAAU,QAAQ;CAIvE,IAAI;CACJ,MAAM,eAAkB,SACtB,KAAK,OAAO,UAAmB;AAC7B,wBAAsB,EAAE,OAAO;AAC/B,QAAM;GACN;CACJ,MAAM,SAAS,MAAM,WACnB,OAAO,IACP;EAAE,OAAO,MAAM;EAAU;EAAQ,EACjC;EACE,aAAa,EACX,UAAU,WAAW,YACnB,YAAY,YAAY,QAAQ,WAAW,QAAQ,CAAC,EACvD;EACD,aAAa,iBAAiB,MAAM,SAAS;EAC7C,UAAU,YAAY,OAAO,OAAO,GAAG;EACvC,YAAY,gBAAgB,MAAM;EAClC,oBAAoB,YAAY,iBAAiB,CAAC;EAClD;EACD,CACF;AACD,KAAI,kBAAmB,OAAM,kBAAkB;AAE/C,KAAI,QAAQ,QAAS;AAErB,KAAI,OAAO,YAAA,UAAgC,OAAM,OAAO,SAAS;AACjE,KAAI,OAAO,YAAA,WAAgC;AACzC,MAAI,OAAO,SAAS,gBAClB,OAAM,cAAc,OAAO,QAAQ,gBAAgB;AAGrD,QAAM,eAAe,OAAO;GAC1B,GAAG,OAAO;GACV,QAAQ,OAAO,YAAA,YAAiC,cAAc;GAC/D,CAAC;AACF;;AAGF,KAAI;AACF,QAAM,UAAU,SAAS,UAAU;UAC5B,OAAO;AACd,YAAU,6CAA6C,MAAM"}
|
|
1
|
+
{"version":3,"file":"run-Cmn1xaWK.js","names":[],"sources":["../src/tui/workflow.ts","../src/tui/run.ts"],"sourcesContent":["/** The TUI's answers to the steps `runProgram` waits on: its screens, gates and overlays. */\nimport { RunOutcome } from '@programs';\nimport type { ProgramId, ProgramWorkflowConnector } from '@programs/types';\nimport type { WizardStore } from './store.js';\n\nexport function tuiWorkflow(\n store: WizardStore,\n programId: ProgramId,\n): ProgramWorkflowConnector {\n return {\n async confirmStep(step) {\n switch (step.kind) {\n case 'ai-approval':\n // AiOptInRequiredScreen holds here, before any source leaves the machine.\n await store.getGate('ai-opt-in');\n return true;\n case 'service-outage':\n // The health-check screen shows the outage; the run stops.\n store.setReadinessResult(step.readiness);\n return false;\n case 'settings-conflict':\n await store.showSettingsOverride(step.conflicts, step.fix);\n return true;\n case 'run':\n // Each screen before the run's step settles first; a hidden step doesn't run.\n return store.reachStep(step.stepId);\n }\n },\n finishStep(step, result) {\n // Only a composed sub-run that succeeded completes its step; a failed one keeps its tasks for the outro and the stream. The program's run step follows its phase.\n if (\n step.programId !== programId &&\n result.outcome === RunOutcome.Success\n ) {\n store.completeRunStep(step.stepId);\n }\n },\n };\n}\n","/**\n * The TUI host: the full-screen wizard. It renders its screens over its own\n * store, settles the intro, and calls `runProgram` once with the OAuth login,\n * the WizardAsk screen as the answerer and its screens as the workflow. An\n * intro that hands off to a tool drives the tool's screens instead. The CLI\n * builds the launch values, owns the signals and applies the exit code.\n */\nimport { join } from 'node:path';\nimport {\n buildSession,\n createFileDestination,\n createWizardRunSync,\n getAuditChecks,\n getProgramConfig,\n loadWizardFlags,\n PostHogDestination,\n ProgramAbort,\n RunOutcome,\n runProgram,\n storeInteraction,\n TaskStreamPush,\n} from '@programs';\nimport type { CredentialsProvider, ProgramConfig } from '@programs/types';\nimport { classifyRunFailure } from '@shared/errors';\nimport { OutroKind } from '@shared/outro';\nimport { RunPhase } from '@shared/run-state';\nimport { VERSION } from '@shared/version';\nimport { analytics } from '@utils/analytics';\nimport { runCleanups } from '@utils/cleanup';\nimport { initLogFile, logToFile } from '@utils/debug';\nimport { flushAnalytics } from '@utils/flush-analytics';\nimport {\n registerShutdown,\n startHostExit,\n wizardAbort,\n} from '@host/wizard-abort';\nimport { printAbortOutro } from '@shared/console-log';\nimport { getTuiTool } from '@tui/tools/index';\nimport { abortOnScreens } from './abort.js';\nimport { displayProgress } from './agent-progress.js';\nimport { oauthCredentials } from './auth/login.js';\nimport { isRunFailure } from './mint-failure.js';\nimport { driveTuiTool, localServicesError, reportFatal } from './run-tool.js';\nimport { startTUI } from './start-tui.js';\nimport type { WizardStore } from './store.js';\nimport type { TuiLaunch } from './launch.js';\nimport { tuiWorkflow } from './workflow.js';\n\n/** How long a screen's exit request waits for analytics before it exits anyway. */\nconst EXIT_REPORT_BUDGET_MS = 2000;\n\n/**\n * Run `config` in the TUI. Resolves with the exit code: the run's, a screen's\n * exit request, a decided failure's through `wizardAbort`, or 130 or 143 on a signal.\n */\nexport async function runTui(\n config: ProgramConfig,\n launch: TuiLaunch,\n): Promise<number> {\n const exit = startHostExit();\n let tui: ReturnType<typeof startTUI> | null = null;\n let stream: TaskStreamPush | null = null;\n let unregisterShutdown: (() => void) | undefined;\n // A signal cancels the run and any pending question before the exit.\n const runAbort = new AbortController();\n let exitInProgress = false;\n let signalled = false;\n // A tool the intro handed off to ends the run once its analytics flush.\n let handedOff = false;\n\n // Flush a terminal-phase push so the web app sees the run ended in error\n // rather than hanging on the last \"running\" snapshot. Registered before the\n // stream exists: Ctrl-C on the intro must still restore the terminal and run\n // the cleanups, and there is no run to report yet.\n const onSignal = (signal: 'SIGINT' | 'SIGTERM'): void => {\n if (signalled || exitInProgress || exit.ended) return;\n signalled = true;\n runAbort.abort();\n logToFile('[run-wizard] signal received, flushing task stream');\n // Settings restore is sync fs work, so it runs before a flush that may time out.\n runCleanups();\n if (tui?.store.session.runPhase === RunPhase.Running) {\n tui.store.setRunPhase(RunPhase.Error);\n }\n void Promise.all([\n stream?.shutdown(2000, 'cancelled'),\n analytics.shutdown('cancelled'),\n ])\n .catch(() => logToFile('[run-wizard] cancellation shutdown failed'))\n .finally(() => {\n unregisterShutdown?.();\n try {\n tui?.unmount();\n } catch {\n // terminal may already be torn down\n }\n exit.end(signal === 'SIGTERM' ? 143 : 130);\n });\n };\n const onAbort = () =>\n onSignal(launch.signal.reason === 'SIGTERM' ? 'SIGTERM' : 'SIGINT');\n // A signal that landed before this host subscribed ends the run too.\n if (launch.signal.aborted) onAbort();\n else launch.signal.addEventListener('abort', onAbort, { once: true });\n\n // Before the TUI mounts: once Ink owns the alt screen, anything written\n // to it is wiped on unmount (see the catch block below), so an abort here\n // would leave the user on a loading screen with no message.\n const localError = await localServicesError(launch.session.baseUrl);\n if (signalled) return exit.exited;\n if (localError) {\n wizardAbort(\n { present: printAbortOutro, exit },\n { message: localError },\n ).catch((error: unknown) => exit.fail(error));\n return exit.exited;\n }\n\n const main = async (): Promise<void> => {\n // Ink handles Ctrl-C itself in raw mode; it arrives here as an interrupt.\n tui = startTUI(VERSION, config.id, () => onSignal('SIGINT'));\n const activeTui = tui;\n const { store } = activeTui;\n // A screen's exit request unmounts, reports the run's end within a bounded wait, then exits with no stream shutdown.\n store.subscribe(() => {\n const code = store.exitRequest;\n if (code === null || handedOff || exitInProgress || signalled) return;\n exitInProgress = true;\n launch.signal.removeEventListener('abort', onAbort);\n activeTui.unmount();\n const report = async (): Promise<void> => {\n try {\n await analytics.shutdown(code === 0 ? 'cancelled' : 'error');\n } catch {\n logToFile('[run-wizard] exit request shutdown failed');\n }\n await flushAnalytics();\n };\n void Promise.race([\n report(),\n new Promise((resolve) => setTimeout(resolve, EXIT_REPORT_BUDGET_MS)),\n ]).then(() => exit.end(code));\n });\n\n const session = buildSession(launch.session);\n if (launch.skillId) {\n session.skillId = launch.skillId;\n } else if (config.skillId) {\n session.skillId = config.skillId;\n }\n store.launch(session, launch.session, config.id);\n\n const credentials = launch.credentials ?? oauthCredentials(store);\n launch.onStore?.(store);\n\n // Detection, then the intro, where the user may switch program.\n for (;;) {\n try {\n await store.runReadyHooks();\n } catch (error) {\n if (!(error instanceof ProgramAbort)) throw error;\n await abortOnScreens(store, {\n code: error.code,\n message: error.message,\n outroData: error.outroData,\n });\n }\n await store.getGate('intro');\n const active = store.router.activeProgram;\n if (active === config.id) break;\n const tool = getTuiTool(active);\n if (tool) {\n handedOff = true;\n const code = await driveTuiTool(store, tool, {\n toolId: active,\n signal: runAbort.signal,\n });\n if (signalled || exit.ended) return;\n exitInProgress = true;\n launch.signal.removeEventListener('abort', onAbort);\n activeTui.unmount();\n await flushAnalytics();\n exit.end(code);\n return;\n }\n config = getProgramConfig(active);\n }\n\n // After the switch loop, not before: the stream bakes its program id,\n // session id, and event-plan path in at construction, so a stream built\n // for the launch program would report the whole run under a program the\n // user left on the intro screen. Nothing before this point produces a\n // task to push.\n // Consent gates the push, not the dump: `--no-telemetry` still logs.\n const fileDestination = createFileDestination(launch.taskStreamLog);\n const destinations = [\n ...(session.noTelemetry\n ? []\n : [\n new PostHogDestination({\n getCredentials: () => store.session.credentials,\n onError: (err) => logToFile('[task-stream-push]', err.message),\n }),\n ]),\n ...(fileDestination ? [fileDestination] : []),\n ];\n const programConfig = config;\n const activeStream = new TaskStreamPush({\n store: store.sessions,\n getFlags: () => analytics.getCachedWizardFlags(),\n programId: programConfig.streamWorkflowId ?? programConfig.id,\n runSync: createWizardRunSync({\n mode: 'local',\n programId: programConfig.id,\n assignedId: launch.runId,\n noTelemetry: session.noTelemetry,\n getSession: () => store.session,\n }),\n destinations,\n eventPlanPath: programConfig.eventPlanFile\n ? join(session.installDir, programConfig.eventPlanFile)\n : undefined,\n auditChecks: programConfig.auditLedgerFile\n ? () => getAuditChecks(store.session)\n : undefined,\n enabled: destinations.length > 0,\n });\n stream = activeStream;\n activeStream.attach();\n unregisterShutdown = registerShutdown((outcome) => {\n if (store.session.runPhase === RunPhase.Running) {\n store.setRunPhase(RunPhase.Error);\n }\n return activeStream.shutdown(2000, outcome);\n });\n\n await store.getGate('integration-check');\n await store.getGate('health-check');\n\n try {\n await runProgramOnScreens(programConfig, store, {\n credentials,\n signal: runAbort.signal,\n });\n } catch (error) {\n // The run threw before its own error handling rendered an outro.\n // Show the handoff screen and let the user's agent take over.\n const failure = classifyRunFailure(error);\n logToFile('[run-wizard] run failed, handing off:', error);\n runCleanups();\n analytics.captureException(\n error instanceof Error ? error : new Error(String(error)),\n { error_code: failure.code },\n );\n store.showOutroError({\n kind: OutroKind.Error,\n errorCode: failure.code,\n message: failure.message,\n });\n }\n\n if (signalled) return;\n const runFailed = isRunFailure(store.session);\n await activeStream.finishRun(runFailed ? 'failed' : 'completed');\n await store.waitUntil((s) => s.mintHandoff === 'exit' || s.skillsComplete);\n // A screen already ended the run (KeepSkills after a success): start no flush it would cut off.\n if (exit.ended || exitInProgress) return;\n\n exitInProgress = true;\n await activeStream.shutdown(2000);\n unregisterShutdown?.();\n launch.signal.removeEventListener('abort', onAbort);\n if (runFailed) await analytics.shutdown('error');\n activeTui.unmount();\n exit.end(runFailed ? 1 : 0);\n };\n\n main().catch(async (err: unknown) => {\n if (signalled || exit.ended) return;\n // File-log first — the cleanup below can throw or exit.\n logToFile('[run-wizard] FATAL:', err);\n // Run cleanups before anything async so settings are restored even if\n // the stream shutdown hangs.\n runCleanups();\n // The task-stream debounce timer keeps the event loop alive, so\n // we have to drain it before exiting on the error path.\n exitInProgress = true;\n launch.signal.removeEventListener('abort', onAbort);\n try {\n await stream?.shutdown(2000, 'failed');\n } catch {\n // ignore\n }\n unregisterShutdown?.();\n try {\n tui?.unmount();\n } catch {\n // ignore\n }\n reportFatal(err);\n exit.end(1);\n });\n\n return exit.exited;\n}\n\n/**\n * The program's run, answered by the screens: `credentials` logs in, the\n * WizardAsk screen answers, and the flow's screens settle each step. A failure\n * shows its outro and ends the run through `wizardAbort`.\n */\nexport async function runProgramOnScreens(\n config: ProgramConfig,\n store: WizardStore,\n {\n credentials,\n signal,\n }: { credentials: CredentialsProvider; signal?: AbortSignal },\n): Promise<void> {\n initLogFile();\n logToFile(`[agent-runner] START ${config.id} build=${analytics.build}`);\n\n // runProgram turns a throwing capability into a failed run; the TUI shows\n // the handoff for those, so the throw is kept and rethrown.\n let capabilityFailure: { error: unknown } | undefined;\n const keepFailure = <T>(work: Promise<T>): Promise<T> =>\n work.catch((error: unknown) => {\n capabilityFailure ??= { error };\n throw error;\n });\n const result = await runProgram(\n config.id,\n { store: store.sessions, config },\n {\n credentials: {\n resolve: (programId, context) =>\n keepFailure(credentials.resolve(programId, context)),\n },\n interaction: storeInteraction(store.sessions),\n workflow: tuiWorkflow(store, config.id),\n onProgress: displayProgress(store),\n featureFlags: () => keepFailure(loadWizardFlags()),\n signal,\n },\n );\n if (capabilityFailure) throw capabilityFailure.error;\n // A signal cancelled the run; the handler that aborted it settles.\n if (signal?.aborted) return;\n\n if (result.outcome === RunOutcome.Crashed) throw result.failure?.error;\n if (result.outcome !== RunOutcome.Success) {\n if (result.failure?.authErrorDetail) {\n store.showAuthError(result.failure.authErrorDetail);\n }\n // The terminal status follows how the run ended, not whether an Error came back.\n await abortOnScreens(store, {\n ...result.failure,\n status: result.outcome === RunOutcome.Aborted ? 'cancelled' : 'error',\n });\n return;\n }\n // The run already succeeded: a failed flush is logged, never the outcome.\n try {\n await analytics.shutdown('success');\n } catch (error) {\n logToFile('[agent-runner] analytics shutdown failed:', error);\n }\n}\n"],"mappings":"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"}
|