diff --git a/workflows/workflow-generator/index.ts b/workflows/workflow-generator/index.ts index 61dcb9b..3df2ae6 100644 --- a/workflows/workflow-generator/index.ts +++ b/workflows/workflow-generator/index.ts @@ -1,4 +1,5 @@ -import { existsSync, readFileSync } from "node:fs"; +import { existsSync, readFileSync, unlinkSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; import { join } from "node:path"; import type { RoleResult, @@ -63,6 +64,13 @@ function formatSpawnFailure(error: SpawnError): string { return `exit ${error.exitCode} stderr=${error.stderr.slice(0, 400)}`; } +function spawnErrorStreams(error: SpawnError): { stdout: string; stderr: string } { + if (error.kind === "spawn_failed") { + return { stdout: "", stderr: "" }; + } + return { stdout: error.stdout, stderr: error.stderr }; +} + function buildSenseGeneratorReference(): string { const ref = join(WORKFLOWS_DIR, "sense-generator", "index.ts"); if (!existsSync(ref)) { @@ -82,9 +90,12 @@ function lastMetaForRole(messages: WorkflowMessage[], role: string): M | null const roleEntrySchema = z .object({ - name: z.string().describe("Role key / identifier in kebab-case or short snake name"), - description: z.string().describe("What this role does in one or two sentences"), - responsibilities: z.string().describe("Concrete responsibilities, inputs, and outputs for this role"), + name: z.string().default("").describe("Role key / identifier in kebab-case or short snake name"), + description: z.string().default("").describe("What this role does in one or two sentences"), + responsibilities: z + .string() + .default("") + .describe("Concrete responsibilities, inputs, and outputs for this role"), }) .describe("One role in the generated workflow"); @@ -92,14 +103,20 @@ const analystExtractSchema = z .object({ workflowName: z .string() + .default("") .describe("kebab-case package directory name under workflows/, e.g. 'ticket-triage'"), - roles: z.array(roleEntrySchema).describe("Planned roles for the new workflow"), - moderatorFlow: z.string().describe("How the moderator should route between roles; start and exit conditions"), + roles: z.array(roleEntrySchema).default([]).describe("Planned roles for the new workflow"), + moderatorFlow: z + .string() + .default("") + .describe("How the moderator should route between roles; start and exit conditions"), externalDeps: z .string() + .default("") .describe("External tools, CLIs, HTTP APIs, or services the workflow must integrate with"), dataFlow: z .string() + .default("") .describe("How data moves between roles: what each step consumes and produces in content/meta"), }) .describe("Structured workflow specification extracted from the analysis"); @@ -110,7 +127,7 @@ type AnalystMetaItem = { responsibilities: string; }; -type WorkflowGenMeta = { +type WorkflowMeta = { analyst: { userPrompt: string; analysis: string; @@ -133,9 +150,17 @@ type WorkflowGenMeta = { attempt: number; validationLog: string; }; + committer: { + branch: string | null; + commitHash: string | null; + pushed: boolean | null; + skipped: boolean; + error: string | null; + stagedPaths: string[]; + }; }; -const emptyAnalystMeta = (userContent: string): WorkflowGenMeta["analyst"] => ({ +const emptyAnalystMeta = (userContent: string): WorkflowMeta["analyst"] => ({ userPrompt: userContent, analysis: "", workflowName: "", @@ -164,7 +189,7 @@ function scanGeneratedCodePitfalls(source: string): string[] { const issues: string[] = []; if (/\bawait\s+import\s*\(/.test(source)) { issues.push( - "Uses await import() — only allowed in sense-runtime / workflow-worker with a documented comment", + "Uses the await keyword with a parenthesized import() call — only allowed in sense-runtime / workflow-worker with a documented comment", ); } if (/\bimport\s*\(\s*["'`]/.test(source) && !source.includes("Dynamic import required")) { @@ -233,14 +258,198 @@ async function runReviewerValidation( return { ok: true, log: logParts.join("\n\n") }; } -const workflow: WorkflowDefinition = { +function summarizeText(s: string, maxLen: number): string { + const one = s.replace(/\s+/g, " ").trim(); + if (one.length <= maxLen) { + return one; + } + return `${one.slice(0, maxLen - 3)}...`; +} + +function sanitizeBranchSegment(name: string): string { + const t = name + .trim() + .replace(/[^a-zA-Z0-9_-]+/g, "-") + .replace(/^-+|-+$/g, ""); + return t.length > 0 ? t : "workflow"; +} + +function resolveWorkflowNameForCommitter(messages: WorkflowMessage[]): string { + const rev = lastMetaForRole(messages, "reviewer"); + if (rev !== null && rev.workflowName.trim().length > 0) { + return rev.workflowName.trim(); + } + const coder = lastMetaForRole(messages, "coder"); + if (coder !== null && coder.workflowName.trim().length > 0) { + return coder.workflowName.trim(); + } + const analyst = lastMetaForRole(messages, "analyst"); + if (analyst !== null && analyst.workflowName.trim().length > 0) { + return analyst.workflowName.trim(); + } + return ""; +} + +function buildCoreStagePaths( + workflowName: string, + files: WorkflowMeta["coder"]["files"], + includeNerveYaml: boolean, +): string[] { + const base = `workflows/${workflowName}`; + const paths: string[] = []; + if (files.indexTs) { + paths.push(`${base}/index.ts`); + } + if (files.packageJson) { + paths.push(`${base}/package.json`); + const lockRel = `${base}/pnpm-lock.yaml`; + if (existsSync(join(NERVE_ROOT, lockRel))) { + paths.push(lockRel); + } + } + if (files.tsconfigJson) { + paths.push(`${base}/tsconfig.json`); + } + if (includeNerveYaml) { + paths.push("nerve.yaml"); + } + return paths; +} + +async function nerveYamlShouldBeStaged(workflowName: string, start: StartStep): Promise { + const dry = isDryRun(start); + if (dry) { + return verifyNerveWorkflowEntry(workflowName).ok; + } + const st = await spawnSafe("git", ["status", "--porcelain", "--", "nerve.yaml"], { + cwd: NERVE_ROOT, + env: null, + timeoutMs: 30_000, + dryRun: false, + }); + if (st.ok && st.value.stdout.trim().length > 0) { + return true; + } + const d1 = await spawnSafe("git", ["diff", "--name-only", "--", "nerve.yaml"], { + cwd: NERVE_ROOT, + env: null, + timeoutMs: 30_000, + dryRun: false, + }); + if (d1.ok && d1.value.stdout.trim().length > 0) { + return true; + } + const d2 = await spawnSafe("git", ["diff", "--cached", "--name-only", "--", "nerve.yaml"], { + cwd: NERVE_ROOT, + env: null, + timeoutMs: 30_000, + dryRun: false, + }); + return d2.ok && d2.value.stdout.trim().length > 0; +} + +async function listUntrackedUnderWorkflowDir(workflowName: string, start: StartStep): Promise { + const dry = isDryRun(start); + if (dry) { + return []; + } + const prefix = `workflows/${workflowName}/`; + const r = await spawnSafe("git", ["status", "--porcelain", "-u", "--", prefix], { + cwd: NERVE_ROOT, + env: null, + timeoutMs: 30_000, + dryRun: false, + }); + if (!r.ok) { + return []; + } + const out: string[] = []; + for (const line of r.value.stdout.split("\n")) { + const t = line.trimEnd(); + if (t.startsWith("?? ")) { + const p = t.slice(3).trim(); + if (p.startsWith(prefix)) { + out.push(p); + } + } + } + return out; +} + +async function resolveDefaultBranchName(start: StartStep, logLines: string[]): Promise { + const dry = isDryRun(start); + const sym = await spawnSafe("git", ["symbolic-ref", "refs/remotes/origin/HEAD"], { + cwd: NERVE_ROOT, + env: null, + timeoutMs: 10_000, + dryRun: dry, + }); + if (sym.ok) { + const out = sym.value.stdout.trim(); + if (out.length > 0 && !out.includes("[dryRun]")) { + const m = out.match(/refs\/remotes\/origin\/(.+)$/); + if (m !== null && m[1] !== undefined && m[1].length > 0) { + logLines.push(`[branch] default via symbolic-ref: ${m[1]}`); + return m[1]; + } + } + } + if (dry) { + logLines.push("[branch] dry-run: assuming default branch name `main`"); + return "main"; + } + const abbrev = await spawnSafe("git", ["rev-parse", "--abbrev-ref", "origin/HEAD"], { + cwd: NERVE_ROOT, + env: null, + timeoutMs: 10_000, + dryRun: dry, + }); + if (abbrev.ok) { + const line = abbrev.value.stdout.trim(); + if (line.length > 0 && line.includes("/")) { + const parts = line.split("/"); + const last = parts[parts.length - 1]; + if (last !== undefined && last.length > 0) { + logLines.push(`[branch] default via origin/HEAD: ${last}`); + return last; + } + } + } + for (const b of ["main", "master"] as const) { + const v = await spawnSafe("git", ["rev-parse", "--verify", `refs/remotes/origin/${b}`], { + cwd: NERVE_ROOT, + env: null, + timeoutMs: 10_000, + dryRun: dry, + }); + if (v.ok) { + logLines.push(`[branch] default fallback: origin/${b} exists`); + return b; + } + } + logLines.push("[branch] default fallback: main (no origin/* resolved)"); + return "main"; +} + +function appendIoSnippet(logLines: string[], label: string, stdout: string, stderr: string): void { + const so = stdout.trim().slice(0, 500); + const se = stderr.trim().slice(0, 500); + if (so.length > 0) { + logLines.push(`${label} stdout (truncated): ${so}`); + } + if (se.length > 0) { + logLines.push(`${label} stderr (truncated): ${se}`); + } +} + +const workflow: WorkflowDefinition = { name: "workflow-generator", roles: { async analyst( start: StartStep, _messages: WorkflowMessage[], - ): Promise> { + ): Promise> { const dry = isDryRun(start); const userInput = start.content; const empty = emptyAnalystMeta(userInput); @@ -368,7 +577,7 @@ Output a thorough analysis in markdown. Do not write final implementation code.` async architect( start: StartStep, messages: WorkflowMessage[], - ): Promise> { + ): Promise> { const dry = isDryRun(start); if (dry) { return { @@ -376,8 +585,13 @@ Output a thorough analysis in markdown. Do not write final implementation code.` meta: { workflowName: "dry-run-test", design: "(dry-run design)" }, }; } - const last = messages[messages.length - 1]; - const spec = last.meta as WorkflowGenMeta["analyst"]; + const spec = lastMetaForRole(messages, "analyst"); + if (spec === null) { + return { + content: "Architect skipped — no analyst output in message history.", + meta: { workflowName: "", design: "" }, + }; + } const wfName = spec.workflowName.trim(); if (wfName.length === 0) { @@ -485,17 +699,21 @@ Output ONLY the design markdown.`; async coder( start: StartStep, messages: WorkflowMessage[], - ): Promise> { + ): Promise> { const dry = isDryRun(start); if (dry) { return { content: "[dry-run] coder complete", - meta: { workflowName: "dry-run-test", generatedFiles: ["(dry-run)"], codegenLog: "(dry-run)" }, + meta: { + workflowName: "dry-run-test", + files: { indexTs: false, packageJson: false, tsconfigJson: false }, + cursorOutput: "(dry-run)", + }, }; } - const analystMeta = lastMetaForRole(messages, "analyst"); - const architectMeta = lastMetaForRole(messages, "architect"); - const priorReviewer = lastMetaForRole(messages, "reviewer"); + const analystMeta = lastMetaForRole(messages, "analyst"); + const architectMeta = lastMetaForRole(messages, "architect"); + const priorReviewer = lastMetaForRole(messages, "reviewer"); if (analystMeta === null || architectMeta === null) { return { @@ -608,19 +826,39 @@ Implement now.`; async reviewer( start: StartStep, messages: WorkflowMessage[], - ): Promise> { + ): Promise> { const dry = isDryRun(start); if (dry) { + const attempt = messages.filter((m) => m.role === "reviewer").length + 1; return { - content: "[dry-run] reviewer complete — LGTM", - meta: { workflowName: "dry-run-test", approved: true, issues: "" }, + content: "[dry-run] reviewer complete — validation skipped; treating as PASS", + meta: { + passed: true, + workflowName: "dry-run-test", + reason: "Dry-run: reviewer validation not executed", + attempt, + validationLog: "(dry-run)", + }, }; } - const last = messages[messages.length - 1]; - const { workflowName, files } = last.meta as WorkflowGenMeta["coder"]; - + const coderEntry = lastMetaForRole(messages, "coder"); const attempt = messages.filter((m) => m.role === "reviewer").length + 1; + if (coderEntry === null) { + return { + content: "FAIL — no coder message in history", + meta: { + passed: false, + workflowName: "", + reason: "Reviewer could not find a prior coder step", + attempt, + validationLog: "", + }, + }; + } + + const { workflowName, files } = coderEntry; + const missing: string[] = []; if (!files.indexTs) missing.push("index.ts"); if (!files.packageJson) missing.push("package.json"); @@ -679,6 +917,348 @@ Implement now.`; }, }; }, + + async committer( + start: StartStep, + messages: WorkflowMessage[], + ): Promise> { + const dry = isDryRun(start); + const logLines: string[] = []; + const gitDir = join(NERVE_ROOT, ".git"); + const nullMeta = (): WorkflowMeta["committer"] => ({ + branch: null, + commitHash: null, + pushed: null, + skipped: true, + error: null, + stagedPaths: [], + }); + + logLines.push("[1] Check `.git` at NERVE_ROOT"); + if (!existsSync(gitDir)) { + logLines.push(`Result: no .git at ${NERVE_ROOT} — skipping all git operations.`); + return { + content: logLines.join("\n"), + meta: nullMeta(), + }; + } + + const analystMeta = lastMetaForRole(messages, "analyst"); + const userPrompt = analystMeta?.userPrompt ?? ""; + const reviewerMeta = lastMetaForRole(messages, "reviewer"); + const coderMeta = lastMetaForRole(messages, "coder"); + const files = + coderMeta !== null + ? coderMeta.files + : { indexTs: false, packageJson: false, tsconfigJson: false }; + + const wfName = resolveWorkflowNameForCommitter(messages); + if (wfName.length === 0) { + logLines.push("ERROR: could not resolve workflowName from analyst/coder/reviewer meta."); + return { + content: logLines.join("\n"), + meta: { + branch: null, + commitHash: null, + pushed: null, + skipped: false, + error: "Empty workflowName — cannot infer paths to stage", + stagedPaths: [], + }, + }; + } + + const includeNerve = await nerveYamlShouldBeStaged(wfName, start); + const untracked = await listUntrackedUnderWorkflowDir(wfName, start); + const corePaths = buildCoreStagePaths(wfName, files, includeNerve); + const plannedSet = new Set(corePaths); + for (const u of untracked) { + plannedSet.add(u); + } + const plannedPaths = [...plannedSet].filter( + (p) => p === "nerve.yaml" || existsSync(join(NERVE_ROOT, p)), + ); + + const dryPlanPaths = + plannedPaths.length > 0 + ? plannedPaths + : buildCoreStagePaths( + wfName, + { indexTs: true, packageJson: true, tsconfigJson: true }, + verifyNerveWorkflowEntry(wfName).ok, + ); + + if (plannedPaths.length === 0 && !dry) { + logLines.push("No candidate paths to `git add` (all file flags false and nerve.yaml not staged)."); + return { + content: logLines.join("\n"), + meta: { + branch: null, + commitHash: null, + pushed: null, + skipped: false, + error: "Nothing to stage for this workflow", + stagedPaths: [], + }, + }; + } + + const defaultBranch = await resolveDefaultBranchName(start, logLines); + const shortSuffix = Date.now().toString(36); + const newBranch = `wf/${sanitizeBranchSegment(wfName)}-${shortSuffix}`; + + if (dry) { + logLines.push("[dry-run] Would run: `git checkout ` then `git checkout -b " + newBranch + "`"); + logLines.push(`[dry-run] Would run: \`git add -- ${dryPlanPaths.join(" ")}\``); + logLines.push("[dry-run] Would run: `git commit` with message summarizing workflow + user prompt + reviewer"); + logLines.push(`[dry-run] Would run: \`git push -u origin ${newBranch}\``); + return { + content: logLines.join("\n"), + meta: { + branch: newBranch, + commitHash: null, + pushed: null, + skipped: false, + error: null, + stagedPaths: dryPlanPaths, + }, + }; + } + + logLines.push(`[2] git checkout ${defaultBranch}`); + const checkoutBase = await spawnSafe("git", ["checkout", defaultBranch], { + cwd: NERVE_ROOT, + env: null, + timeoutMs: 120_000, + dryRun: false, + }); + if (!checkoutBase.ok) { + const err = formatSpawnFailure(checkoutBase.error); + const io = spawnErrorStreams(checkoutBase.error); + appendIoSnippet(logLines, "checkout", io.stdout, io.stderr); + logLines.push(`ERROR: ${err}`); + return { + content: logLines.join("\n"), + meta: { + branch: null, + commitHash: null, + pushed: null, + skipped: false, + error: `git checkout ${defaultBranch}: ${err}`, + stagedPaths: [], + }, + }; + } + appendIoSnippet(logLines, "checkout", checkoutBase.value.stdout, checkoutBase.value.stderr); + + logLines.push(`[3] git checkout -b ${newBranch}`); + const checkoutNew = await spawnSafe("git", ["checkout", "-b", newBranch], { + cwd: NERVE_ROOT, + env: null, + timeoutMs: 120_000, + dryRun: false, + }); + if (!checkoutNew.ok) { + const err = formatSpawnFailure(checkoutNew.error); + const ioNb = spawnErrorStreams(checkoutNew.error); + appendIoSnippet(logLines, "checkout -b", ioNb.stdout, ioNb.stderr); + logLines.push(`ERROR: ${err}`); + return { + content: logLines.join("\n"), + meta: { + branch: null, + commitHash: null, + pushed: null, + skipped: false, + error: `git checkout -b: ${err}`, + stagedPaths: [], + }, + }; + } + appendIoSnippet(logLines, "checkout -b", checkoutNew.value.stdout, checkoutNew.value.stderr); + + logLines.push(`[4] git add -- ${plannedPaths.join(" ")}`); + const addArgs = ["add", "--", ...plannedPaths]; + const addR = await spawnSafe("git", addArgs, { + cwd: NERVE_ROOT, + env: null, + timeoutMs: 120_000, + dryRun: false, + }); + if (!addR.ok) { + const err = formatSpawnFailure(addR.error); + const ioAdd = spawnErrorStreams(addR.error); + appendIoSnippet(logLines, "git add", ioAdd.stdout, ioAdd.stderr); + logLines.push(`ERROR: ${err}`); + return { + content: logLines.join("\n"), + meta: { + branch: newBranch, + commitHash: null, + pushed: null, + skipped: false, + error: `git add: ${err}`, + stagedPaths: [], + }, + }; + } + appendIoSnippet(logLines, "git add", addR.value.stdout, addR.value.stderr); + + const stagedR = await spawnSafe("git", ["diff", "--cached", "--name-only"], { + cwd: NERVE_ROOT, + env: null, + timeoutMs: 30_000, + dryRun: false, + }); + const stagedPaths = stagedR.ok + ? stagedR.value.stdout + .split("\n") + .map((s) => s.trim()) + .filter((s) => s.length > 0) + : []; + + const userBrief = summarizeText(userPrompt, 200); + const reasonBrief = + reviewerMeta !== null ? summarizeText(reviewerMeta.reason, 240) : "(no reviewer reason)"; + const subject = `workflow: ${wfName}`; + const body = + `Workflow: ${wfName}\n` + + `User request (summary): ${userBrief}\n` + + `Reviewer (summary): ${reasonBrief}\n` + + `Staged paths:\n${stagedPaths.map((p) => `- ${p}`).join("\n") || "(none)"}\n`; + const commitMessage = `${subject}\n\n${body}`; + + const msgPath = join(tmpdir(), `nerve-workflow-generator-commit-${Date.now()}.txt`); + logLines.push(`[5] git commit -F ${msgPath}`); + let commitHash: string | null = null; + let commitErr: string | null = null; + try { + writeFileSync(msgPath, commitMessage, "utf-8"); + const commitR = await spawnSafe("git", ["commit", "-F", msgPath], { + cwd: NERVE_ROOT, + env: null, + timeoutMs: 120_000, + dryRun: false, + }); + if (!commitR.ok) { + commitErr = `git commit: ${formatSpawnFailure(commitR.error)}`; + const ioCommit = spawnErrorStreams(commitR.error); + appendIoSnippet(logLines, "git commit", ioCommit.stdout, ioCommit.stderr); + logLines.push(`ERROR: ${commitErr}`); + } else { + appendIoSnippet(logLines, "git commit", commitR.value.stdout, commitR.value.stderr); + const revR = await spawnSafe("git", ["rev-parse", "HEAD"], { + cwd: NERVE_ROOT, + env: null, + timeoutMs: 10_000, + dryRun: false, + }); + if (!revR.ok) { + commitErr = `git rev-parse HEAD: ${formatSpawnFailure(revR.error)}`; + logLines.push(`ERROR: ${commitErr}`); + } else { + commitHash = revR.value.stdout.trim() || null; + logLines.push(`[6] commit hash: ${commitHash ?? "(empty)"}`); + } + } + } finally { + try { + unlinkSync(msgPath); + } catch { + /* ignore */ + } + } + + if (commitErr !== null) { + return { + content: logLines.filter(Boolean).join("\n"), + meta: { + branch: newBranch, + commitHash, + pushed: null, + skipped: false, + error: commitErr, + stagedPaths, + }, + }; + } + + logLines.push("[7] git remote get-url origin"); + const urlR = await spawnSafe("git", ["remote", "get-url", "origin"], { + cwd: NERVE_ROOT, + env: null, + timeoutMs: 10_000, + dryRun: false, + }); + if (!urlR.ok) { + logLines.push(`No origin remote (skip push): ${formatSpawnFailure(urlR.error)}`); + return { + content: logLines.filter(Boolean).join("\n"), + meta: { + branch: newBranch, + commitHash, + pushed: null, + skipped: false, + error: null, + stagedPaths, + }, + }; + } + const originUrl = urlR.value.stdout.trim(); + if (originUrl.length === 0) { + logLines.push("origin URL empty — skip push"); + return { + content: logLines.filter(Boolean).join("\n"), + meta: { + branch: newBranch, + commitHash, + pushed: null, + skipped: false, + error: null, + stagedPaths, + }, + }; + } + logLines.push(`origin: ${originUrl}`); + + logLines.push(`[8] git push -u origin ${newBranch}`); + const pushR = await spawnSafe("git", ["push", "-u", "origin", newBranch], { + cwd: NERVE_ROOT, + env: null, + timeoutMs: 300_000, + dryRun: false, + }); + if (!pushR.ok) { + const pe = `git push: ${formatSpawnFailure(pushR.error)}`; + const ioPush = spawnErrorStreams(pushR.error); + appendIoSnippet(logLines, "git push", ioPush.stdout, ioPush.stderr); + logLines.push(`ERROR: ${pe}`); + return { + content: logLines.filter(Boolean).join("\n"), + meta: { + branch: newBranch, + commitHash, + pushed: false, + skipped: false, + error: pe, + stagedPaths, + }, + }; + } + appendIoSnippet(logLines, "git push", pushR.value.stdout, pushR.value.stderr); + + return { + content: logLines.filter(Boolean).join("\n"), + meta: { + branch: newBranch, + commitHash, + pushed: true, + skipped: false, + error: null, + stagedPaths, + }, + }; + }, }, moderator(context) { @@ -707,15 +1287,19 @@ Implement now.`; } if (last.role === "reviewer") { - if (last.meta.passed) { - return END; + if (last.meta.passed === true) { + return "committer"; } - if (last.meta.attempt < 3) { + if (last.meta.passed === false && last.meta.attempt < 3) { return "coder"; } return END; } + if (last.role === "committer") { + return END; + } + return END; }, }; diff --git a/workflows/workflow-generator/package.json b/workflows/workflow-generator/package.json index 1cba9cc..69a378b 100644 --- a/workflows/workflow-generator/package.json +++ b/workflows/workflow-generator/package.json @@ -9,7 +9,8 @@ "zod": "^4.3.6" }, "devDependencies": { - "@types/node": "^22.0.0" + "@types/node": "^22.0.0", + "typescript": "^5.7.0" }, "pnpm": { "overrides": { diff --git a/workflows/workflow-generator/pnpm-lock.yaml b/workflows/workflow-generator/pnpm-lock.yaml index 15302ba..237146e 100644 --- a/workflows/workflow-generator/pnpm-lock.yaml +++ b/workflows/workflow-generator/pnpm-lock.yaml @@ -26,12 +26,20 @@ importers: '@types/node': specifier: ^22.0.0 version: 22.19.17 + typescript: + specifier: ^5.7.0 + version: 5.9.3 packages: '@types/node@22.19.17': resolution: {integrity: sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==} + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} @@ -44,6 +52,8 @@ snapshots: dependencies: undici-types: 6.21.0 + typescript@5.9.3: {} + undici-types@6.21.0: {} zod@4.3.6: {}