diff --git a/nerve.yaml b/nerve.yaml index b534ad2..308fbde 100644 --- a/nerve.yaml +++ b/nerve.yaml @@ -10,11 +10,22 @@ senses: throttle: 30s timeout: 30s grace_period: null + hermes-session-message-stats: + group: hermes + throttle: 30s + timeout: 60s + grace_period: null workflows: sense-generator: concurrency: 1 overflow: drop + workflow-generator: + concurrency: 1 + overflow: drop + pr-summarizer: + concurrency: 1 + overflow: drop reflexes: - kind: sense @@ -23,3 +34,6 @@ reflexes: - kind: sense sense: hermes-gateway-health interval: 2m + - kind: sense + sense: hermes-session-message-stats + interval: 15m diff --git a/workflows/workflow-generator/index.ts b/workflows/workflow-generator/index.ts new file mode 100644 index 0000000..625ec74 --- /dev/null +++ b/workflows/workflow-generator/index.ts @@ -0,0 +1,690 @@ +import { existsSync, readFileSync } from "node:fs"; +import { join } from "node:path"; +import type { + RoleResult, + StartStep, + WorkflowDefinition, + WorkflowMessage, +} from "@uncaged/nerve-core"; +import { END, parseNerveConfig } from "@uncaged/nerve-core"; +import type { SpawnError } from "@uncaged/nerve-workflow-utils"; +import { + cursorAgent, + isDryRun, + llmExtract, + nerveAgentContext, + readNerveYaml, + spawnSafe, +} from "@uncaged/nerve-workflow-utils"; +import { z } from "zod"; + +const HOME = process.env.HOME ?? "/home/azureuser"; +const NERVE_ROOT = join(HOME, ".uncaged-nerve"); +const WORKFLOWS_DIR = join(NERVE_ROOT, "workflows"); + +function getNerveYaml(): string { + const result = readNerveYaml({ nerveRoot: NERVE_ROOT }); + return result.ok ? result.value : "# nerve.yaml unavailable"; +} + +async function cfgGet(key: string): Promise { + const result = await spawnSafe("cfg", ["get", key], { + cwd: NERVE_ROOT, + env: null, + timeoutMs: 10_000, + }); + if (!result.ok) { + return null; + } + return result.value.stdout.trim() || null; +} + +async function resolveDashScopeProvider(): Promise<{ + baseUrl: string; + apiKey: string; + model: string; +} | null> { + const apiKey = process.env.DASHSCOPE_API_KEY ?? (await cfgGet("DASHSCOPE_API_KEY")); + const baseUrl = process.env.DASHSCOPE_BASE_URL ?? (await cfgGet("DASHSCOPE_BASE_URL")); + const model = process.env.DASHSCOPE_MODEL ?? (await cfgGet("DASHSCOPE_MODEL")) ?? "qwen-plus"; + if (!apiKey || !baseUrl) { + return null; + } + return { apiKey, baseUrl, model }; +} + +function formatSpawnFailure(error: SpawnError): string { + if (error.kind === "spawn_failed") { + return error.message; + } + if (error.kind === "timeout") { + return `timeout (stdout=${error.stdout.slice(0, 200)})`; + } + return `exit ${error.exitCode} stderr=${error.stderr.slice(0, 400)}`; +} + +function buildSenseGeneratorReference(): string { + const ref = join(WORKFLOWS_DIR, "sense-generator", "index.ts"); + if (!existsSync(ref)) { + return "(reference file workflows/sense-generator/index.ts not found)"; + } + return readFileSync(ref, "utf-8"); +} + +function lastMetaForRole(messages: WorkflowMessage[], role: string): M | null { + for (let i = messages.length - 1; i >= 0; i--) { + if (messages[i].role === role) { + return messages[i].meta as M; + } + } + return 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"), + }) + .describe("One role in the generated workflow"); + +const analystExtractSchema = z + .object({ + workflowName: z + .string() + .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"), + externalDeps: z + .string() + .describe("External tools, CLIs, HTTP APIs, or services the workflow must integrate with"), + dataFlow: z + .string() + .describe("How data moves between roles: what each step consumes and produces in content/meta"), + }) + .describe("Structured workflow specification extracted from the analysis"); + +type AnalystMetaItem = { + name: string; + description: string; + responsibilities: string; +}; + +type WorkflowGenMeta = { + analyst: { + userPrompt: string; + analysis: string; + workflowName: string; + roles: AnalystMetaItem[]; + moderatorFlow: string; + externalDeps: string; + dataFlow: string; + }; + architect: { workflowName: string; design: string }; + coder: { + workflowName: string; + files: { indexTs: boolean; packageJson: boolean; tsconfigJson: boolean }; + cursorOutput: string; + }; + reviewer: { + passed: boolean; + workflowName: string; + reason: string; + attempt: number; + validationLog: string; + }; +}; + +const emptyAnalystMeta = (userContent: string): WorkflowGenMeta["analyst"] => ({ + userPrompt: userContent, + analysis: "", + workflowName: "", + roles: [], + moderatorFlow: "", + externalDeps: "", + dataFlow: "", +}); + +function verifyNerveWorkflowEntry(workflowName: string): { ok: true } | { ok: false; reason: string } { + const readResult = readNerveYaml({ nerveRoot: NERVE_ROOT }); + if (!readResult.ok) { + return { ok: false, reason: `readNerveYaml: ${readResult.error.message}` }; + } + const parsed = parseNerveConfig(readResult.value); + if (!parsed.ok) { + return { ok: false, reason: `parseNerveConfig: ${parsed.error.message}` }; + } + if (parsed.value.workflows[workflowName] === undefined) { + return { ok: false, reason: `nerve.yaml has no workflows.${workflowName} entry` }; + } + return { ok: true }; +} + +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", + ); + } + if (/\bimport\s*\(\s*["'`]/.test(source) && !source.includes("Dynamic import required")) { + issues.push("Dynamic import() without documented exception comment"); + } + if (/\bexport\s+default\s+/.test(source) === false) { + issues.push("Missing default export of WorkflowDefinition (engine loads the default export)"); + } + return issues; +} + +async function runReviewerValidation( + workflowDir: string, + workflowName: string, + dry: boolean, +): Promise<{ ok: true; log: string } | { ok: false; log: string; reason: string }> { + const logParts: string[] = []; + + const indexPath = join(workflowDir, "index.ts"); + const pkgPath = join(workflowDir, "package.json"); + const tsconfigPath = join(workflowDir, "tsconfig.json"); + if (!existsSync(indexPath) || !existsSync(pkgPath) || !existsSync(tsconfigPath)) { + const miss: string[] = []; + if (!existsSync(indexPath)) miss.push("index.ts"); + if (!existsSync(pkgPath)) miss.push("package.json"); + if (!existsSync(tsconfigPath)) miss.push("tsconfig.json"); + return { ok: false, log: "", reason: `Missing required file(s): ${miss.join(", ")}` }; + } + + const source = readFileSync(indexPath, "utf-8"); + const pitfalls = scanGeneratedCodePitfalls(source); + if (pitfalls.length > 0) { + const pitfallText = pitfalls.join("\n"); + logParts.push(`=== static checks ===\n${pitfallText}`); + return { ok: false, log: logParts.join("\n\n"), reason: pitfallText }; + } + + const tsc = await spawnSafe("npx", ["tsc", "--noEmit"], { + cwd: workflowDir, + env: null, + timeoutMs: 300_000, + dryRun: dry, + }); + if (!tsc.ok) { + const msg = formatSpawnFailure(tsc.error); + logParts.push(`=== npx tsc --noEmit ===\n${msg}`); + return { ok: false, log: logParts.join("\n\n"), reason: `Typecheck failed: ${msg}` }; + } + const tscOut = tsc.value.stderr.trim() || tsc.value.stdout.trim() || "(no output)"; + logParts.push(`=== npx tsc --noEmit ===\n${tscOut}`); + + const nerveCheck = verifyNerveWorkflowEntry(workflowName); + if (!nerveCheck.ok) { + logParts.push(`=== nerve.yaml ===\n${nerveCheck.reason}`); + return { + ok: false, + log: logParts.join("\n\n"), + reason: `nerve.yaml: ${nerveCheck.reason}`, + }; + } + logParts.push(`=== nerve.yaml ===\nworkflows.${workflowName} is present.`); + + const importLines = source.split("\n").filter((l) => /^\s*import\s/.test(l)); + logParts.push(`=== import lines ===\n${importLines.join("\n")}`); + + return { ok: true, log: logParts.join("\n\n") }; +} + +const workflow: WorkflowDefinition = { + name: "workflow-generator", + + roles: { + async analyst( + start: StartStep, + _messages: WorkflowMessage[], + ): Promise> { + const dry = isDryRun(start); + const userInput = start.content; + const empty = emptyAnalystMeta(userInput); + + const provider = await resolveDashScopeProvider(); + if (provider === null) { + return { + content: + "Cannot run analyst: set DASHSCOPE_API_KEY and DASHSCOPE_BASE_URL (or configure via `cfg get`), " + + "and optionally DASHSCOPE_MODEL.", + meta: empty, + }; + } + + const askPrompt = `You are analyzing a user request to build a new Nerve **workflow** (multi-role automaton with a moderator). + +${nerveAgentContext} + +User's natural language description: +${userInput} + +Nerve root: ${NERVE_ROOT} +Target workflows live under: ${WORKFLOWS_DIR}// + +## Your task +- Clarify the goal, constraints, and success criteria. +- Identify a good kebab-case workflow package name. +- Propose a role breakdown: what each role should do, in order. +- Describe how a moderator should route between roles and when to end. +- List external tools/APIs and how data should flow in \`content\` vs \`meta\` between roles. + +Current nerve.yaml (for context only; do not edit here): +\`\`\`yaml +${getNerveYaml()} +\`\`\` + +For reference, here is a complete existing workflow (patterns to mirror, not to copy literally): +\`\`\`ts +${buildSenseGeneratorReference().slice(0, 18_000)} +\`\`\` + +Output a thorough analysis in markdown. Do not write final implementation code.`; + + const planResult = await cursorAgent({ + prompt: askPrompt, + mode: "ask", + cwd: NERVE_ROOT, + env: null, + timeoutMs: null, + dryRun: dry, + }); + if (!planResult.ok) { + return { + content: `cursor-agent failed: ${formatSpawnFailure(planResult.error)}`, + meta: { ...empty, analysis: "" }, + }; + } + const analysis = planResult.value; + + const extracted = await llmExtract({ + text: analysis, + schema: analystExtractSchema, + provider, + dryRun: dry, + }); + if (!extracted.ok) { + return { + content: `${analysis}\n\n[llmExtract error] ${JSON.stringify(extracted.error)}`, + meta: { + userPrompt: userInput, + analysis, + workflowName: "", + roles: [], + moderatorFlow: "", + externalDeps: "", + dataFlow: "", + }, + }; + } + + const e = extracted.value; + const summary = + `## Analysis\n\n${analysis}\n\n` + + `## Structured spec\n\n` + + `**workflowName:** ${e.workflowName}\n\n` + + `**moderatorFlow:**\n${e.moderatorFlow}\n\n` + + `**externalDeps:**\n${e.externalDeps}\n\n` + + `**dataFlow:**\n${e.dataFlow}\n\n` + + `**roles:**\n` + + e.roles + .map( + (r, i) => + `${i + 1}. **${r.name}** — ${r.description}\n - ${r.responsibilities}`, + ) + .join("\n\n"); + + return { + content: summary, + meta: { + userPrompt: userInput, + analysis, + workflowName: e.workflowName, + roles: e.roles, + moderatorFlow: e.moderatorFlow, + externalDeps: e.externalDeps, + dataFlow: e.dataFlow, + }, + }; + }, + + async architect( + start: StartStep, + messages: WorkflowMessage[], + ): Promise> { + const dry = isDryRun(start); + const last = messages[messages.length - 1]; + const spec = last.meta as WorkflowGenMeta["analyst"]; + const wfName = spec.workflowName.trim(); + + if (wfName.length === 0) { + return { + content: "Architect skipped — analyst did not produce a workflow name.", + meta: { workflowName: "", design: "" }, + }; + } + + const rolesText = spec.roles + .map( + (r) => + `### ${r.name}\n- **description:** ${r.description}\n- **responsibilities:** ${r.responsibilities}`, + ) + .join("\n\n"); + + const designPrompt = `You are the architect for a new Nerve **workflow** (multi-role state machine with a \`WorkflowDefinition\` and moderator). + +${nerveAgentContext} + +Target package directory: ${WORKFLOWS_DIR}/${wfName}/ + +## Analyst output + +**User prompt:** +${spec.userPrompt} + +**Moderator / routing (from analyst):** +${spec.moderatorFlow} + +**External dependencies:** +${spec.externalDeps} + +**Data flow:** +${spec.dataFlow} + +**Roles (planned):** +${rolesText} + +## Your task (design document only, no file contents) + +Produce an implementation-ready design in markdown: + +1. **Meta type (TypeScript)** + - A concrete \`type WorkflowMeta = { ... }\` using \`type\` (not interface), no optional \`?:\` — use \`T | null\` for nullable fields. + - One entry per role with the exact fields each role will put in \`RoleResult\` meta. + +2. **Role functions** + - For each role: parameters (\`StartStep\`, \`WorkflowMessage[]\`), return \`RoleResult<…>\`, what to read from \`start\` / prior messages, what to put in \`content\` vs \`meta\`. + +3. **Moderator** + - Pseudocode for \`moderator(context)\` using \`END\` from \`@uncaged/nerve-core\`, edge conditions, and error paths (routed in moderator, not via process exit). + +4. **Error handling** + - How each role reports recoverable failure (content + meta) and how the moderator steers the thread. + +5. **Imports** + - List required imports from \`@uncaged/nerve-core\` and \`@uncaged/nerve-workflow-utils\` only as needed by the final code. + +6. **Files the coder will write** + - \`${WORKFLOWS_DIR}/${wfName}/index.ts\` — \`export default\` a \`WorkflowDefinition\` + - \`${WORKFLOWS_DIR}/${wfName}/package.json\` with \`"type": "module"\` and dependencies (include \`zod\` if the workflow parses structured data) + - \`${WORKFLOWS_DIR}/${wfName}/tsconfig.json\` — if \`${NERVE_ROOT}/tsconfig.workflow.base.json\` exists, extend it; else a strict NodeNext \`noEmit\` project + +7. **nerve.yaml** + - The coder must add a \`workflows:${wfName}\` block to \`${NERVE_ROOT}/nerve.yaml\` (concurrency, overflow) without removing existing keys. + +8. **Nerve code rules to preserve in the generated \`index.ts\`** + - No dynamic \`import()\` in the generated workflow (except documented exceptions in engine loaders). + - \`type\` over \`interface\`, \`function\` over \`class\` for the workflow’s own code. + +## Reference (meta-workflow style) +\`\`\`ts +${buildSenseGeneratorReference().slice(0, 22_000)} +\`\`\` + +Current nerve.yaml: +\`\`\`yaml +${getNerveYaml()} +\`\`\` + +Output ONLY the design markdown.`; + + const planResult = await cursorAgent({ + prompt: designPrompt, + mode: "ask", + cwd: NERVE_ROOT, + env: null, + timeoutMs: null, + dryRun: dry, + }); + if (!planResult.ok) { + return { + content: `cursor-agent failed: ${formatSpawnFailure(planResult.error)}`, + meta: { workflowName: wfName, design: "" }, + }; + } + + return { + content: planResult.value, + meta: { workflowName: wfName, design: planResult.value }, + }; + }, + + async coder( + start: StartStep, + messages: WorkflowMessage[], + ): Promise> { + const dry = isDryRun(start); + const analystMeta = lastMetaForRole(messages, "analyst"); + const architectMeta = lastMetaForRole(messages, "architect"); + const priorReviewer = lastMetaForRole(messages, "reviewer"); + + if (analystMeta === null || architectMeta === null) { + return { + content: "coder: missing analyst or architect message in history", + meta: { + workflowName: "", + files: { indexTs: false, packageJson: false, tsconfigJson: false }, + cursorOutput: "", + }, + }; + } + + const wfName = analystMeta.workflowName.trim(); + if (wfName.length === 0) { + return { + content: "coder: empty workflow name", + meta: { + workflowName: "", + files: { indexTs: false, packageJson: false, tsconfigJson: false }, + cursorOutput: "", + }, + }; + } + + const fixSection = + priorReviewer !== null && priorReviewer.passed === false + ? `\n\n## Previous review (address these before anything else)\n${priorReviewer.reason}\n\nFull validation log:\n${priorReviewer.validationLog}\n` + : ""; + + const codePrompt = `You are implementing a new Nerve workflow package at ${WORKFLOWS_DIR}/${wfName}/. + +## Architect design (authoritative for structure) +${architectMeta.design} + +## Analyst structured fields +${JSON.stringify( + { + workflowName: analystMeta.workflowName, + userPrompt: analystMeta.userPrompt, + roles: analystMeta.roles, + moderatorFlow: analystMeta.moderatorFlow, + externalDeps: analystMeta.externalDeps, + dataFlow: analystMeta.dataFlow, + }, + null, + 2, +)} +${fixSection} + +## Files to create or update +1. \`${WORKFLOWS_DIR}/${wfName}/index.ts\` — \`export default\` a \`WorkflowDefinition\` (same style as sense-generator: named imports, default export at end). +2. \`${WORKFLOWS_DIR}/${wfName}/package.json\` — \`"type": "module"\`, dependencies on \`@uncaged/nerve-core\`, \`@uncaged/nerve-workflow-utils\`, \`zod\` if used; add \`typescript\` in devDependencies so \`npx tsc --noEmit\` works in that directory. +3. \`${WORKFLOWS_DIR}/${wfName}/tsconfig.json\` — strict, \`module\`/\`moduleResolution\` NodeNext, \`noEmit: true\`, include all \`.ts\` in the folder. + +4. **Register the workflow** — merge a new block into the existing \`${NERVE_ROOT}/nerve.yaml\` under the top-level \`workflows:\` key: + \`\`\`yaml + ${wfName}: + concurrency: 1 + overflow: drop + \`\`\` + Do not remove or overwrite unrelated senses, reflexes, or other workflow entries. Preserve valid YAML. + +## Implementation patterns (when applicable) +- \`resolveDashScopeProvider\`, \`nerveAgentContext\`, \`readNerveYaml\`, \`cursorAgent\`, \`llmExtract\`, \`spawnSafe\`, \`formatSpawnFailure\` from \`@uncaged/nerve-workflow-utils\` as in sense-generator. +- No dynamic \`import()\` in the new workflow code. + +## Reference workflow +\`\`\`ts +${buildSenseGeneratorReference().slice(0, 20_000)} +\`\`\` + +Current nerve.yaml (merge carefully; keep all existing content): +\`\`\`yaml +${getNerveYaml()} +\`\`\` + +Implement now.`; + + const agentResult = await cursorAgent({ + prompt: codePrompt, + mode: "default", + cwd: NERVE_ROOT, + env: null, + timeoutMs: null, + dryRun: dry, + }); + + const workflowDir = join(WORKFLOWS_DIR, wfName); + const files = { + indexTs: existsSync(join(workflowDir, "index.ts")), + packageJson: existsSync(join(workflowDir, "package.json")), + tsconfigJson: existsSync(join(workflowDir, "tsconfig.json")), + }; + + if (!agentResult.ok) { + const errText = `cursor-agent failed: ${formatSpawnFailure(agentResult.error)}`; + return { + content: errText, + meta: { workflowName: wfName, files, cursorOutput: errText }, + }; + } + + return { + content: agentResult.value, + meta: { workflowName: wfName, files, cursorOutput: agentResult.value }, + }; + }, + + async reviewer( + start: StartStep, + messages: WorkflowMessage[], + ): Promise> { + const dry = isDryRun(start); + const last = messages[messages.length - 1]; + const { workflowName, files } = last.meta as WorkflowGenMeta["coder"]; + + const attempt = messages.filter((m) => m.role === "reviewer").length + 1; + + const missing: string[] = []; + if (!files.indexTs) missing.push("index.ts"); + if (!files.packageJson) missing.push("package.json"); + if (!files.tsconfigJson) missing.push("tsconfig.json"); + if (missing.length > 0) { + return { + content: `FAIL — missing: ${missing.join(", ")}`, + meta: { + passed: false, + workflowName, + reason: `Missing required file(s): ${missing.join(", ")}`, + attempt, + validationLog: "", + }, + }; + } + + const name = workflowName.trim(); + if (name.length === 0) { + return { + content: "FAIL — empty workflow name in coder meta", + meta: { + passed: false, + workflowName: "", + reason: "Coder meta had empty workflowName", + attempt, + validationLog: "", + }, + }; + } + + const workflowDir = join(WORKFLOWS_DIR, name); + const checks = await runReviewerValidation(workflowDir, name, dry); + + if (!checks.ok) { + return { + content: `FAIL — ${checks.reason}`, + meta: { + passed: false, + workflowName: name, + reason: checks.reason, + attempt, + validationLog: checks.log, + }, + }; + } + + return { + content: `PASS — typecheck and nerve.yaml check OK.\n\n${checks.log.slice(0, 8000)}`, + meta: { + passed: true, + workflowName: name, + reason: "npx tsc --noEmit passed and nerve.yaml contains the workflow entry", + attempt, + validationLog: checks.log, + }, + }; + }, + }, + + moderator(context) { + if (context.steps.length === 0) { + return "analyst"; + } + + const last = context.steps[context.steps.length - 1]; + + if (last.role === "analyst") { + if (last.meta.workflowName.trim().length === 0) { + return END; + } + return "architect"; + } + + if (last.role === "architect") { + if (last.meta.workflowName.trim().length === 0 || last.meta.design.trim().length === 0) { + return END; + } + return "coder"; + } + + if (last.role === "coder") { + return "reviewer"; + } + + if (last.role === "reviewer") { + if (last.meta.passed) { + return END; + } + if (last.meta.attempt < 3) { + return "coder"; + } + return END; + } + + return END; + }, +}; + +export default workflow; diff --git a/workflows/workflow-generator/package.json b/workflows/workflow-generator/package.json new file mode 100644 index 0000000..1cba9cc --- /dev/null +++ b/workflows/workflow-generator/package.json @@ -0,0 +1,21 @@ +{ + "name": "workflow-generator-workflow", + "version": "0.0.1", + "private": true, + "type": "module", + "dependencies": { + "@uncaged/nerve-core": "latest", + "@uncaged/nerve-workflow-utils": "latest", + "zod": "^4.3.6" + }, + "devDependencies": { + "@types/node": "^22.0.0" + }, + "pnpm": { + "overrides": { + "@uncaged/nerve-daemon": "link:../../../repos/nerve/packages/daemon", + "@uncaged/nerve-core": "link:../../../repos/nerve/packages/core", + "@uncaged/nerve-workflow-utils": "link:../../../repos/nerve/packages/workflow-utils" + } + } +} diff --git a/workflows/workflow-generator/pnpm-lock.yaml b/workflows/workflow-generator/pnpm-lock.yaml new file mode 100644 index 0000000..15302ba --- /dev/null +++ b/workflows/workflow-generator/pnpm-lock.yaml @@ -0,0 +1,49 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +overrides: + '@uncaged/nerve-daemon': link:../../../repos/nerve/packages/daemon + '@uncaged/nerve-core': link:../../../repos/nerve/packages/core + '@uncaged/nerve-workflow-utils': link:../../../repos/nerve/packages/workflow-utils + +importers: + + .: + dependencies: + '@uncaged/nerve-core': + specifier: link:../../../repos/nerve/packages/core + version: link:../../../repos/nerve/packages/core + '@uncaged/nerve-workflow-utils': + specifier: link:../../../repos/nerve/packages/workflow-utils + version: link:../../../repos/nerve/packages/workflow-utils + zod: + specifier: ^4.3.6 + version: 4.3.6 + devDependencies: + '@types/node': + specifier: ^22.0.0 + version: 22.19.17 + +packages: + + '@types/node@22.19.17': + resolution: {integrity: sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==} + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + zod@4.3.6: + resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} + +snapshots: + + '@types/node@22.19.17': + dependencies: + undici-types: 6.21.0 + + undici-types@6.21.0: {} + + zod@4.3.6: {} diff --git a/workflows/workflow-generator/tsconfig.json b/workflows/workflow-generator/tsconfig.json new file mode 100644 index 0000000..fc00159 --- /dev/null +++ b/workflows/workflow-generator/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022"], + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "skipLibCheck": true, + "noEmit": true, + "types": ["node"] + }, + "include": ["./**/*.ts"] +}