-
Notifications
You must be signed in to change notification settings - Fork 0
fix(generation): move master workflow payload into sidecars #133
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
01e5a55
f8d60c3
9b884dc
b4d8015
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -182,6 +182,16 @@ export function renderMasterExecutionWorkflow(input: RenderMasterWorkflowInput): | |
| const gates = buildMasterGates(artifactsDir, plan, input.spec); | ||
| const skillApplicationEvidence = buildMasterSkillEvidence(input.skills); | ||
| const toolSelections = buildMasterToolSelections(plan); | ||
| // Sidecars travel with the generated workflow on disk. Embedding the spec | ||
| // and the child-source map inline ballooned the master workflow past | ||
| // ARG_MAX (spawn E2BIG) every time `materialize-child-workflows` ran; the | ||
| // sidecars push that payload off of argv entirely. | ||
| const artifactPathNoExt = artifactPath.replace(/\.ts$/, ''); | ||
| const specSidecarPath = `${artifactPathNoExt}.spec.md`; | ||
| const childrenSidecarPath = `${artifactPathNoExt}.children.json`; | ||
| const childSources = Object.fromEntries( | ||
| plan.children.map((child) => [child.workflowFilePath, childWorkflowSource(child, input.spec, specSidecarPath)]), | ||
| ); | ||
| const content = renderMasterSource({ | ||
| spec: input.spec, | ||
| pattern: input.pattern, | ||
|
|
@@ -191,6 +201,9 @@ export function renderMasterExecutionWorkflow(input: RenderMasterWorkflowInput): | |
| plan, | ||
| skills: input.skills, | ||
| skillApplicationEvidence, | ||
| childSources, | ||
| specSidecarPath, | ||
| childrenSidecarPath, | ||
| }); | ||
|
|
||
| return { | ||
|
|
@@ -210,6 +223,10 @@ export function renderMasterExecutionWorkflow(input: RenderMasterWorkflowInput): | |
| skillMatches: input.skills.matches, | ||
| toolSelections, | ||
| artifactsDir, | ||
| sidecarFiles: { | ||
| [specSidecarPath]: input.spec.description, | ||
| [childrenSidecarPath]: `${JSON.stringify(childSources, null, 2)}\n`, | ||
| }, | ||
| }, | ||
| }; | ||
| } | ||
|
|
@@ -240,10 +257,10 @@ function renderMasterSource(input: { | |
| plan: MasterExecutionPlan; | ||
| skills: SkillContext; | ||
| skillApplicationEvidence: SkillApplicationEvidence[]; | ||
| childSources: Record<string, string>; | ||
| specSidecarPath: string; | ||
| childrenSidecarPath: string; | ||
| }): string { | ||
| const childSources = Object.fromEntries( | ||
| input.plan.children.map((child) => [child.workflowFilePath, childWorkflowSource(child, input.spec)]), | ||
| ); | ||
| const planJson = JSON.stringify(input.plan, null, 2); | ||
| const skillMatchesJson = JSON.stringify(input.skills.matches, null, 2); | ||
| const skillBoundaryJson = JSON.stringify({ | ||
|
|
@@ -253,11 +270,14 @@ function renderMasterSource(input: { | |
| loadedSkills: input.skills.applicableSkillNames, | ||
| applicationEvidence: input.skillApplicationEvidence, | ||
| }, null, 2); | ||
| // Read the child source map from disk at runtime instead of inlining the | ||
| // JSON into this shell command. Inlining once N children deep produced a | ||
| // multi-megabyte argv element and triggered `spawn E2BIG`. | ||
| const materializeCommand = [ | ||
| 'node --input-type=module <<\'NODE\'', | ||
| 'import { mkdirSync, writeFileSync } from \'node:fs\';', | ||
| 'import { mkdirSync, writeFileSync, readFileSync } from \'node:fs\';', | ||
| 'import { dirname } from \'node:path\';', | ||
| `const childSources = ${JSON.stringify(childSources, null, 2)};`, | ||
| `const childSources = JSON.parse(readFileSync(${literal(input.childrenSidecarPath)}, 'utf8'));`, | ||
| 'for (const [filePath, source] of Object.entries(childSources)) {', | ||
| ' mkdirSync(dirname(filePath), { recursive: true });', | ||
| ' writeFileSync(filePath, source, \'utf8\');', | ||
|
|
@@ -330,6 +350,12 @@ function renderMasterSource(input: { | |
| 'echo RICKY_MASTER_FINAL_VALIDATION_READY', | ||
| ]; | ||
|
|
||
| // The master workflow's `.description()` is sent to runtime agents as | ||
| // context. Inlining the full spec text here meant every agent invocation | ||
| // carried ~100KB+ of markdown that the agents did not need (the spec lives | ||
| // on disk at specSidecarPath and child slices `cp` it locally). The short | ||
| // ref keeps the description small without losing the pointer. | ||
| const masterDescription = `${firstHeadingOrSummary(input.spec.description)} (full spec on disk at ${input.specSidecarPath}; child workflows read it from there).`; | ||
| return `${[ | ||
| "import { workflow } from '@agent-relay/sdk/workflows';", | ||
| '', | ||
|
|
@@ -339,7 +365,7 @@ function renderMasterSource(input: { | |
| '', | ||
| 'async function main() {', | ||
| ` const result = await workflow(${literal(input.workflowId)})`, | ||
| ` .description(${literal(input.spec.description)})`, | ||
| ` .description(${literal(masterDescription)})`, | ||
| ` .pattern(${literal(input.pattern.pattern)})`, | ||
| ` .channel(${literal(input.channel)})`, | ||
| ' .maxConcurrency(4)', | ||
|
|
@@ -471,7 +497,7 @@ function renderChildRunStep(child: ChildWorkflowPlan): string[] { | |
| ]; | ||
| } | ||
|
|
||
| export function childWorkflowSource(child: ChildWorkflowPlan, spec: NormalizedWorkflowSpec): string { | ||
| export function childWorkflowSource(child: ChildWorkflowPlan, spec: NormalizedWorkflowSpec, specSidecarPath?: string): string { | ||
| const artifactsDir = child.signoffArtifactPath.replace(/\/signoff\.md$/, ''); | ||
| const validationCommand = child.validationCommands[0] ?? 'npm run typecheck'; | ||
| const targetScope = child.targetFiles.length > 0 ? child.targetFiles.join(' ') : 'NO_TARGET_FILES_DECLARED'; | ||
|
|
@@ -515,7 +541,14 @@ export function childWorkflowSource(child: ChildWorkflowPlan, spec: NormalizedWo | |
| ` command: ${literal([ | ||
| 'set -e', | ||
| `mkdir -p ${shellQuote(artifactsDir)}`, | ||
| `printf '%s\\n' ${shellQuote(spec.description)} > ${shellQuote(`${artifactsDir}/normalized-spec.txt`)}`, | ||
| // Copy the spec from its sidecar file rather than embedding it via | ||
| // `printf '%s\\n' '<huge spec text>'`. The embedded form pushed every | ||
| // child's prepare-context heredoc to ~100KB and made the materialize | ||
| // step's argv multi-megabyte. When the sidecar path isn't known | ||
| // (legacy call sites), fall back to a no-op so this child still runs. | ||
| specSidecarPath | ||
| ? `cp ${shellQuote(specSidecarPath)} ${shellQuote(`${artifactsDir}/normalized-spec.txt`)}` | ||
| : `: > ${shellQuote(`${artifactsDir}/normalized-spec.txt`)}`, | ||
| `printf '%s\\n' ${shellQuote(targetScope)} > ${shellQuote(`${artifactsDir}/target-files.txt`)}`, | ||
| // Snapshot the worktree's dirty set BEFORE this child touches anything. | ||
| // The master executor runs every child in the SAME checkout, so by the | ||
|
|
@@ -899,6 +932,58 @@ function literal(value: string | string[]): string { | |
| return JSON.stringify(value); | ||
| } | ||
|
|
||
| function firstHeadingOrSummary(specText: string): string { | ||
| const candidate = firstMarkdownHeadingOrParagraph(specText) | ||
| ?? specText.split('\n').find((line) => line.trim().length > 0)?.trim() | ||
| ?? 'Ricky master workflow'; | ||
| return candidate.length > 200 ? `${candidate.slice(0, 197)}...` : candidate; | ||
| } | ||
|
Comment on lines
+935
to
+940
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win Use mdast traversal for heading extraction, not regex line matching. This helper currently inspects markdown with As per coding guidelines, "Default to AST walk for the file's grammar (TS, mdast, JSON.parse) before using tokenizers, regex, or substring matching when analyzing source text in Ricky implementation." 🤖 Prompt for AI Agents |
||
|
|
||
| function firstMarkdownHeadingOrParagraph(specText: string): string | undefined { | ||
| let root: Nodes; | ||
| try { | ||
| root = fromMarkdown(specText); | ||
| } catch { | ||
| return undefined; | ||
| } | ||
|
|
||
| let fallback: string | undefined; | ||
| const visit = (node: Nodes): string | undefined => { | ||
| if (node.type === 'heading' && 'depth' in node && (node.depth === 1 || node.depth === 2)) { | ||
| const text = markdownNodeText(node); | ||
| if (text) return text; | ||
| } | ||
|
|
||
| if (!fallback && (node.type === 'paragraph' || node.type === 'text')) { | ||
| fallback = markdownNodeText(node); | ||
| } | ||
|
|
||
| if ('children' in node) { | ||
| for (const child of node.children) { | ||
| const found = visit(child); | ||
| if (found) return found; | ||
| } | ||
| } | ||
|
|
||
| return undefined; | ||
| }; | ||
|
|
||
| return visit(root) ?? fallback; | ||
| } | ||
|
|
||
| function markdownNodeText(node: Nodes): string | undefined { | ||
| if ('value' in node && typeof node.value === 'string') { | ||
| return node.value.replace(/\s+/g, ' ').trim() || undefined; | ||
| } | ||
| if (!('children' in node)) return undefined; | ||
| const text = node.children | ||
| .map((child) => markdownNodeText(child) ?? '') | ||
| .join(' ') | ||
| .replace(/\s+/g, ' ') | ||
| .trim(); | ||
| return text || undefined; | ||
| } | ||
|
|
||
| function templateLiteral(value: string): string { | ||
| return `\`${value.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$\{/g, '\\${')}\``; | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Enforce repo-root confinement before writing sidecars.
sidecarPathis written without validating it stays inside the invocation root. A malformed/absolute path can write outside the workspace.🔒 Proposed hardening
📝 Committable suggestion
🤖 Prompt for AI Agents