diff --git a/src/product/generation/pipeline.test.ts b/src/product/generation/pipeline.test.ts index 203c83a1..ee94196c 100644 --- a/src/product/generation/pipeline.test.ts +++ b/src/product/generation/pipeline.test.ts @@ -8,6 +8,7 @@ const RECEIVED_AT = '2026-04-26T00:00:00.000Z'; interface SpecFixtureOverrides { description?: string; + targetContext?: string; targetFiles?: string[]; constraints?: string[]; evidenceRequirements?: string[]; @@ -1092,7 +1093,8 @@ describe('workflow generation pipeline', () => { }); expect(result.success).toBe(true); - expect(result.artifact?.content).toContain('Non-goals:'); + expect(result.artifact?.content).toContain('.workflow-artifacts/generated/linear-scope/non-goals.md'); + expect(result.artifact?.content).toContain('# Non-goals'); expect(result.artifact?.content).toContain('- Non-goal: Passive Linear comment monitoring'); expect(result.artifact?.content).toContain('Use this exact section heading in the lead plan.'); }); @@ -1350,6 +1352,85 @@ describe('workflow generation pipeline', () => { expect(content).toContain(`${artifactsDir}/skill-application-boundary.json`); expect(content).toContain(`${artifactsDir}/skill-runtime-boundary.txt`); expect(content).toContain(`${artifactsDir}/signoff.md`); + expect(content).toContain(`${artifactsDir}/normalized-spec.md`); + expect(content).toContain(`${artifactsDir}/acceptance-contract.json`); + expect(content).toContain(`${artifactsDir}/lead-plan-instructions.md`); + expect(content).toContain(`${artifactsDir}/implementation-instructions.md`); + expect(content).toContain(`${artifactsDir}/review-checklist.md`); + }); + + it('packages long spec context into sidecar files instead of agent task bodies', () => { + const longSpecSentinel = 'VERY_LONG_INLINE_SENTINEL_SHOULD_ONLY_LIVE_IN_CONTEXT_PACKAGE'; + const longDescription = [ + 'Implement prompt packaging for generated workflow context.', + longSpecSentinel, + 'This paragraph is intentionally repeated so generated agent tasks must point at sidecar files instead of carrying the entire normalized spec.', + ].join(' ').repeat(80); + const result = generate({ + spec: spec({ + description: longDescription, + targetFiles: ['src/product/generation/template-renderer.ts'], + acceptanceGates: ['npx vitest run src/product/generation/pipeline.test.ts'], + }), + artifactPath: 'workflows/generated/packaged-context.ts', + }); + + expect(result.success).toBe(true); + const content = artifact(result).content; + expect(content).toContain('.workflow-artifacts/generated/packaged-context/normalized-spec.md'); + expect(content).toContain('.workflow-artifacts/generated/packaged-context/acceptance-contract.json'); + + const prepareContextCommand = renderedStepCommand(content, 'prepare-context'); + expect(prepareContextCommand).not.toContain(longSpecSentinel); + expect(prepareContextCommand).not.toContain('printf'); + expect(prepareContextCommand.length).toBeLessThan(2000); + expect(renderedStepCommands(content).join('\n')).not.toContain(longSpecSentinel); + + const taskBodies = renderedTaskBodies(content); + expect(taskBodies.length).toBeGreaterThan(0); + expect(taskBodies.join('\n')).not.toContain(longSpecSentinel); + expect(Math.max(...taskBodies.map((body) => body.length))).toBeLessThan(2500); + }); + + it('threads target context sidecars into planning and review prompts', () => { + const result = generate({ + spec: spec({ + description: 'Implement a workflow using the supplied target context.', + targetContext: 'docs/product/ricky-simplified-workflow-cli-spec.md', + targetFiles: ['src/product/generation/template-renderer.ts'], + }), + artifactPath: 'workflows/generated/target-context-aware.ts', + }); + + expect(result.success).toBe(true); + const content = artifact(result).content; + const taskBodies = renderedTaskBodies(content); + + expect(taskBodies.some((body) => body.includes('lead-plan-instructions.md') && body.includes('target-context.txt'))).toBe(true); + expect(taskBodies.some((body) => body.includes('review-checklist.md') && body.includes('target-context.txt'))).toBe(true); + }); + + it('restricts generated target context file reads to repo-relative workspace paths', () => { + const result = generate({ + spec: spec({ + description: 'Implement a workflow using a target context path.', + targetContext: '../outside-workspace.md', + targetFiles: ['src/product/generation/template-renderer.ts'], + }), + artifactPath: 'workflows/generated/target-context-path-guard.ts', + }); + + expect(result.success).toBe(true); + const content = artifact(result).content; + + expect(content).toContain('function resolveRickyGeneratedTargetContextPath(value: string): string | null'); + expect(content).toContain('if (rickyWorkflowPath.isAbsolute(value)) return null;'); + expect(content).toContain('const workspaceRoot = rickyWorkflowFs.realpathSync(process.cwd());'); + expect(content).toContain('const candidatePath = rickyWorkflowPath.resolve(workspaceRoot, value);'); + expect(content).toContain('realCandidatePath.startsWith(`${workspaceRoot}${rickyWorkflowPath.sep}`)'); + expect(content).toContain('rickyWorkflowFs.writeFileSync(targetContext.outputPath, ensureTrailingNewline(targetContext.value));'); + expect(content).not.toContain('existsSync(targetContext.value)'); + expect(content).not.toContain('copyFileSync(targetContext.value'); }); }); @@ -1358,6 +1439,22 @@ function artifact(result: ReturnType): NonNullable match[1].replace(/\\`/g, '`')); +} + +function renderedStepCommand(content: string, stepName: string): string { + const stepIndex = content.search(new RegExp(`\\.step\\(${JSON.stringify(stepName)},`)); + expect(stepIndex).toBeGreaterThanOrEqual(0); + const commandMatch = /command:\s*("(?:(?:\\[\s\S])|[^"\\])*")/.exec(content.slice(stepIndex)); + expect(commandMatch).not.toBeNull(); + return JSON.parse(commandMatch![1]) as string; +} + +function renderedStepCommands(content: string): string[] { + return [...content.matchAll(/command:\s*("(?:(?:\\[\s\S])|[^"\\])*")/g)].map((match) => JSON.parse(match[1]) as string); +} + function gate( artifact: NonNullable['artifact']>, name: string, @@ -1381,6 +1478,7 @@ function spec(overrides: SpecFixtureOverrides = {}): NormalizedWorkflowSpec { requestId: rawPayload.requestId, metadata: {}, }; + const targetContext = overrides.targetContext ?? null; const targetFiles = overrides.targetFiles ?? []; const constraints = overrides.constraints ?? []; const evidenceRequirements = overrides.evidenceRequirements ?? []; @@ -1390,7 +1488,7 @@ function spec(overrides: SpecFixtureOverrides = {}): NormalizedWorkflowSpec { intent: 'generate', description, targetRepo: null, - targetContext: null, + targetContext, targetFiles, desiredAction: { kind: 'generate', @@ -1424,7 +1522,7 @@ function spec(overrides: SpecFixtureOverrides = {}): NormalizedWorkflowSpec { intent: { primary: 'generate', signals: ['test fixture'] }, description, targetRepo: undefined, - targetContext: undefined, + targetContext: targetContext ?? undefined, targetFiles, constraints, evidenceRequirements, diff --git a/src/product/generation/template-renderer.ts b/src/product/generation/template-renderer.ts index 99a75000..2aded7d0 100644 --- a/src/product/generation/template-renderer.ts +++ b/src/product/generation/template-renderer.ts @@ -118,6 +118,7 @@ function renderSource(input: { toolSelection: ToolSelectionContext; }): string { const onError = input.pattern.riskLevel === 'low' ? "'fail-fast'" : `'retry', { maxRetries: ${DEFAULT_RETRY_MAX_ATTEMPTS}, retryDelayMs: ${DEFAULT_RETRY_BACKOFF_MS} }`; + const contextSetup = buildGeneratedContextSetup(input.spec, input.artifactsDir, input.pattern, input.skills, input.skillApplicationEvidence, input.toolSelection); const lines: string[] = [ "import { workflow } from '@agent-relay/sdk/workflows';", "import * as rickyWorkflowFs from 'node:fs';", @@ -128,8 +129,11 @@ function renderSource(input: { '', renderWorkflowEnvLoaderHelper(), '', + renderGeneratedContextWriterHelper(), + '', 'async function main() {', ' loadRickyWorkflowEnv();', + renderGeneratedContextWriterCall(contextSetup), ` const result = await workflow(${literal(input.workflowId)})`, ` .description(${literal(input.spec.description)})`, ` .pattern(${literal(input.pattern.pattern)})`, @@ -140,11 +144,11 @@ function renderSource(input: { '', ...input.team.map(renderAgentLine), '', - renderPrepareContextStep(input.spec, input.artifactsDir, input.pattern, input.skills, input.skillApplicationEvidence, input.toolSelection), + renderPrepareContextStep(input.artifactsDir, contextSetup), '', renderGateStep(input.gates.find((gate) => gate.name === 'skill-boundary-metadata-gate')!), '', - renderLeadPlanStep(input.spec, input.artifactsDir), + renderLeadPlanStep(input.artifactsDir, Boolean(input.spec.targetContext)), '', renderGateStep(input.gates.find((gate) => gate.name === 'lead-plan-gate')!), '', @@ -154,7 +158,7 @@ function renderSource(input: { '', renderGateStep(input.gates.find((gate) => gate.name === 'initial-soft-validation')!), '', - renderReviewStep('review-claude', 'reviewer-claude', ['initial-soft-validation'], input.spec, input.artifactsDir, selectionFor(input.toolSelection, 'review-claude')), + renderReviewStep('review-claude', 'reviewer-claude', ['initial-soft-validation'], input.artifactsDir, Boolean(input.spec.targetContext), selectionFor(input.toolSelection, 'review-claude')), '', renderSecondaryReviewStep('review-codex', ['initial-soft-validation'], input.spec, input.artifactsDir, selectionFor(input.toolSelection, 'review-codex'), input.isCodeWorkflow), '', @@ -170,7 +174,7 @@ function renderSource(input: { '', renderGateStep(input.gates.find((gate) => gate.name === 'post-fix-validation')!), '', - renderReviewStep('final-review-claude', 'reviewer-claude', ['post-fix-validation'], input.spec, input.artifactsDir, selectionFor(input.toolSelection, 'final-review-claude'), true), + renderReviewStep('final-review-claude', 'reviewer-claude', ['post-fix-validation'], input.artifactsDir, Boolean(input.spec.targetContext), selectionFor(input.toolSelection, 'final-review-claude'), true), '', renderSecondaryReviewStep('final-review-codex', ['post-fix-validation'], input.spec, input.artifactsDir, selectionFor(input.toolSelection, 'final-review-codex'), input.isCodeWorkflow, true), '', @@ -533,14 +537,22 @@ function renderAgentLine(member: TeamMemberSpec): string { return ` .agent(${literal(member.name)}, { ${options.join(', ')} })`; } -function renderPrepareContextStep( +interface GeneratedContextSetup { + files: GeneratedContextFile[]; + targetContext?: { + value: string; + outputPath: string; + }; +} + +function buildGeneratedContextSetup( spec: NormalizedWorkflowSpec, artifactsDir: string, pattern: PatternDecision, skills: SkillContext, skillApplicationEvidence: SkillApplicationEvidence[], toolSelection: ToolSelectionContext, -): string { +): GeneratedContextSetup { const skillBoundary = { behavior: 'generation_time_only', runtimeEmbodiment: false, @@ -554,75 +566,240 @@ function renderPrepareContextStep( return `${match.id} confidence=${match.confidence} reason=${match.reason} evidence=${evidence}`; }).join('\n') : 'No skills matched the normalized spec.'; - const skillContextCommands = skills.matches + const contextPackage = buildGeneratedContextPackage(spec, artifactsDir, pattern, loadedSkillsReport); + const matchedSkillsContent = skills.matches .filter((match) => match.path) - .flatMap((match) => { + .map((match) => { const skillContent = safeReadText(match.path); - return [ - `printf '%s\\n' ${shellQuote(`\n# ${match.id}\nreason=${match.reason}\n`)} >> ${shellQuote(`${artifactsDir}/matched-skills.md`)}`, - `printf '%s\\n' ${shellQuote(skillContent)} >> ${shellQuote(`${artifactsDir}/matched-skills.md`)}`, - ]; - }); + return `\n# ${match.id}\nreason=${match.reason}\n${skillContent}`; + }) + .join('\n'); + const files = [ + { path: `${artifactsDir}/normalized-spec.txt`, content: spec.description }, + ...contextPackage, + { path: `${artifactsDir}/pattern-decision.txt`, content: `pattern=${pattern.pattern}; reason=${pattern.reason}` }, + { path: `${artifactsDir}/loaded-skills.txt`, content: loadedSkillsReport }, + { path: `${artifactsDir}/skill-matches.json`, content: JSON.stringify(normalizeSkillMatchesForArtifact(skills.matches)) }, + { path: `${artifactsDir}/tool-selection.json`, content: JSON.stringify(toolSelection.selections) }, + { path: `${artifactsDir}/skill-application-boundary.json`, content: JSON.stringify(skillBoundary) }, + { path: `${artifactsDir}/skill-runtime-boundary.txt`, content: skillBoundary.boundary }, + { path: `${artifactsDir}/matched-skills.md`, content: matchedSkillsContent }, + ]; + + return { + files, + ...(spec.targetContext ? { targetContext: { value: spec.targetContext, outputPath: `${artifactsDir}/target-context.txt` } } : {}), + }; +} + +function renderPrepareContextStep(artifactsDir: string, contextSetup: GeneratedContextSetup): string { + const expectedFiles = [ + ...contextSetup.files.map((file) => file.path), + ...(contextSetup.targetContext ? [contextSetup.targetContext.outputPath] : []), + ]; const commands = [ `mkdir -p ${shellQuote(artifactsDir)}`, - `printf '%s\\n' ${shellQuote(spec.description)} > ${shellQuote(`${artifactsDir}/normalized-spec.txt`)}`, - `printf '%s\\n' ${shellQuote(`pattern=${pattern.pattern}; reason=${pattern.reason}`)} > ${shellQuote(`${artifactsDir}/pattern-decision.txt`)}`, - `printf '%s\\n' ${shellQuote(loadedSkillsReport)} > ${shellQuote(`${artifactsDir}/loaded-skills.txt`)}`, - `printf '%s\\n' ${shellQuote(JSON.stringify(normalizeSkillMatchesForArtifact(skills.matches)))} > ${shellQuote(`${artifactsDir}/skill-matches.json`)}`, - `printf '%s\\n' ${shellQuote(JSON.stringify(toolSelection.selections))} > ${shellQuote(`${artifactsDir}/tool-selection.json`)}`, - `printf '%s\\n' ${shellQuote(JSON.stringify(skillBoundary))} > ${shellQuote(`${artifactsDir}/skill-application-boundary.json`)}`, - `printf '%s\\n' ${shellQuote(skillBoundary.boundary)} > ${shellQuote(`${artifactsDir}/skill-runtime-boundary.txt`)}`, - `: > ${shellQuote(`${artifactsDir}/matched-skills.md`)}`, - ...skillContextCommands, - ...(spec.targetContext ? [renderTargetContextCommand(spec.targetContext, `${artifactsDir}/target-context.txt`)] : []), + ...expectedFiles.map((file) => `test -f ${shellQuote(file)}`), 'echo GENERATED_WORKFLOW_CONTEXT_READY', ]; return renderDeterministicStep('prepare-context', [], commands.join(' && '), true); } -function renderTargetContextCommand(targetContext: string, outputPath: string): string { - const quotedContext = shellQuote(targetContext); - const quotedOutput = shellQuote(outputPath); - return `if test -f ${quotedContext}; then cat ${quotedContext} > ${quotedOutput}; else printf '%s\\n' ${quotedContext} > ${quotedOutput}; fi`; +interface GeneratedContextFile { + path: string; + content: string; } -function renderLeadPlanStep(spec: NormalizedWorkflowSpec, artifactsDir: string): string { +function buildGeneratedContextPackage( + spec: NormalizedWorkflowSpec, + artifactsDir: string, + pattern: PatternDecision, + loadedSkillsReport: string, +): GeneratedContextFile[] { const nonGoals = defaultNonGoals(spec); + const deliverables = spec.targetFiles.length > 0 + ? spec.targetFiles + : ['A generated workflow artifact and any requested output files']; + const verificationCommands = [ + 'file_exists gate for declared targets', + 'deterministic sanity gate using POSIX grep, git grep, or an equivalent assertion', + 'active-reference gate for deleted manifest paths', + 'npx tsc --noEmit', + deriveTestCommand(spec), + 'git diff gate comparing git diff --name-status against the declared change inventory and requiring a non-empty diff', + 'PR URL or explicit result summary', + ]; + const acceptanceContract = { + description: spec.description, + desiredAction: spec.desiredAction, + targetFiles: spec.targetFiles, + constraints: spec.constraints, + evidenceRequirements: spec.evidenceRequirements, + acceptanceGates: spec.acceptanceGates, + executionPreference: spec.executionPreference, + pattern: { + selected: pattern.pattern, + reason: pattern.reason, + riskLevel: pattern.riskLevel, + specSignals: pattern.specSignals, + }, + generatedArtifactsDir: artifactsDir, + requiredLeadPlanHeadings: ['Non-goals', 'Routing contract', 'Implementation contract'], + requiredLeadPlanSentinel: 'GENERATION_LEAD_PLAN_READY', + implementationContract: { + sourceChangesRequired: isCodeWritingWorkflow(spec), + requireNonEmptyDiffEvidence: true, + requireResultOrPrReporting: true, + }, + routingContract: { + local: 'Run through Agent Relay using the generated workflow artifact.', + cloud: 'Cloud callers receive the same generated artifact contract unless the normalized spec explicitly requests a separate cloud path.', + mcp: 'Generated runtime agents must not use Relaycast management or messaging tools.', + }, + }; + + return [ + { + path: `${artifactsDir}/normalized-spec.md`, + content: [ + '# Normalized Spec', + '', + spec.description, + '', + '## Target Context', + '', + spec.targetContext ? `See ${artifactsDir}/target-context.txt.` : 'None declared.', + ].join('\n'), + }, + { + path: `${artifactsDir}/acceptance-contract.json`, + content: JSON.stringify(acceptanceContract, null, 2), + }, + { + path: `${artifactsDir}/non-goals.md`, + content: [ + '# Non-goals', + '', + ...formatList(nonGoals).split('\n'), + ].join('\n'), + }, + { + path: `${artifactsDir}/deliverables.md`, + content: [ + '# Deliverables', + '', + '## Declared File Targets', + '', + ...formatList(deliverables).split('\n'), + '', + '## Output Manifest', + '', + spec.targetFiles.length === 0 + ? `Write every changed path to ${artifactsDir}/output-manifest.txt using status-prefixed entries such as "A path", "M path", or "D path".` + : 'Declared target files define the expected source-change boundary.', + ].join('\n'), + }, + { + path: `${artifactsDir}/verification-plan.md`, + content: [ + '# Verification Plan', + '', + 'Run or satisfy these verification requirements before signoff:', + '', + ...formatList(verificationCommands).split('\n'), + '', + 'Generated workflow quality:', + '', + '- Include a real deterministic sanity gate over produced files, not just prose saying one exists.', + '- Prefer POSIX grep, git grep, or a small inline assertion command that exits non-zero when expected content/state is missing.', + '- If using rg, guard it with command -v rg and provide a grep or git grep fallback.', + '- For cleanup or deletion work, persist a changed-files inventory with statuses, active-reference evidence for deleted paths, and command summaries for final signoff.', + `- For cleanup or deletion work, start from ${artifactsDir}/cleanup-candidate-prescan.txt and cite that exact path in ${artifactsDir}/cleanup-report.md so the evidence trail names its prescan input.`, + '- Keep each agent step bounded to one coherent slice. Split broad implementation or test-writing work into sequential/fan-out steps with deterministic gates between them instead of relying on a single long agent timeout.', + ].join('\n'), + }, + { + path: `${artifactsDir}/lead-plan-instructions.md`, + content: [ + '# Lead Plan Instructions', + '', + 'Plan the workflow execution from the packaged context files, not from the short task prompt.', + '', + 'Required sections:', + '', + '- Non-goals', + '- Routing contract', + '- Implementation contract', + '- Deliverables', + '- Verification gates', + '', + 'Use this exact section heading in the lead plan. Do not rename "Non-goals" to "Out of scope" or another synonym.', + '', + `Write ${artifactsDir}/lead-plan.md and end it with GENERATION_LEAD_PLAN_READY.`, + '', + 'Generation-time skill boundary:', + '', + `- Read ${artifactsDir}/skill-application-boundary.json and treat it as generator metadata only.`, + '- Skills are applied by Ricky during selection, loading, and template rendering.', + '- Do not claim generated agents load, retain, or embody skill files at runtime unless a future runtime test proves that path.', + '', + 'Loaded skills summary:', + '', + loadedSkillsReport, + ].join('\n'), + }, + { + path: `${artifactsDir}/implementation-instructions.md`, + content: [ + '# Implementation Instructions', + '', + 'IMPLEMENTATION_WORKFLOW_CONTRACT:', + '', + '- For implementation specs, edit source files and produce code changes, not just plan.md, mapping.json, or analysis artifacts.', + '- Keep a non-empty implementation diff outside transient artifact directories.', + '- Add or update tests that prove the changed behavior.', + '- Keep execution routing explicit for local, cloud, and MCP callers.', + '- Materialize outputs to disk, then stop for deterministic gates.', + ].join('\n'), + }, + { + path: `${artifactsDir}/review-checklist.md`, + content: [ + '# Review Checklist', + '', + 'Assess:', + '', + '- Declared file targets and non-goals.', + '- Deterministic gates and evidence quality.', + '- Review/fix/final-review 80-to-100 loop shape.', + '- Local/cloud/MCP routing clarity.', + '- Whether source changes, tests, non-empty diff evidence, and PR/result reporting satisfy the implementation contract.', + ].join('\n'), + }, + ]; +} + +function renderLeadPlanStep(artifactsDir: string, hasTargetContext: boolean): string { return ` .step('lead-plan', { agent: 'lead-claude', dependsOn: ['skill-boundary-metadata-gate'], timeoutMs: ${DEFAULT_LEAD_PLAN_TIMEOUT_MS}, - task: ${templateLiteral(`Plan the workflow execution from the normalized spec. - -Generation-time skill boundary: -- Read ${artifactsDir}/skill-application-boundary.json and treat it as generator metadata only. -- Skills are applied by Ricky during selection, loading, and template rendering. -- Do not claim generated agents load, retain, or embody skill files at runtime unless a future runtime test proves that path. - -Description: -${spec.description} - -Implementation contract: -- If this is an implementation spec, agents must make source changes in the target repository rather than stopping at planning artifacts. -- Final success requires code/source changes, tests, non-empty diff evidence, and PR/result reporting unless the spec explicitly says planning-only. - -Deliverables: -${formatList(spec.targetFiles.length > 0 ? spec.targetFiles : ['A generated workflow artifact and any requested output files'])} - -Non-goals: -${formatList(nonGoals)} -Use this exact section heading in the lead plan. Do not rename it to "Out of scope" or another synonym. - -Routing contract: -- Local: run through Agent Relay using the generated workflow artifact and persist artifacts under ${artifactsDir}. -- Cloud: no separate cloud execution path is implied unless the normalized spec explicitly requests cloud; cloud callers receive the same generated artifact contract. -- MCP: generated runtime agents must not use Relaycast management or messaging tools; MCP callers receive artifacts without a separate runtime management path. - -Verification commands: -${formatList(['file_exists gate for declared targets', 'deterministic sanity gate using POSIX grep, git grep, or an equivalent assertion', 'active-reference gate for deleted manifest paths', 'npx tsc --noEmit', deriveTestCommand(spec), 'git diff gate comparing git diff --name-status against the declared change inventory and requiring a non-empty diff', 'PR URL or explicit result summary'])} - -Write ${artifactsDir}/lead-plan.md ending with GENERATION_LEAD_PLAN_READY.`)}, + task: ${templateLiteral(`Plan the workflow execution from the packaged context files. + +Read these files in order: +- ${artifactsDir}/lead-plan-instructions.md +- ${artifactsDir}/normalized-spec.md +- ${artifactsDir}/acceptance-contract.json +- ${artifactsDir}/non-goals.md +- ${artifactsDir}/deliverables.md +- ${artifactsDir}/verification-plan.md +- ${artifactsDir}/skill-application-boundary.json +${hasTargetContext ? `- ${artifactsDir}/target-context.txt` : ''} + +Write ${artifactsDir}/lead-plan.md. +Required headings: Non-goals, Routing contract, Implementation contract. +End the file with GENERATION_LEAD_PLAN_READY.`)}, verification: { type: 'output_contains', value: 'GENERATION_LEAD_PLAN_READY' }, })`; } @@ -643,32 +820,21 @@ ${selectionLines} timeoutMs: ${DEFAULT_IMPLEMENT_TIMEOUT_MS}, task: ${templateLiteral(`${isCodeWorkflow ? 'Implement the requested code-writing workflow slice.' : 'Author the requested workflow artifact.'} -IMPLEMENTATION_WORKFLOW_CONTRACT: -- For implementation specs, edit source files and produce code changes, not just plan.md, mapping.json, or analysis artifacts. -- Keep a non-empty implementation diff outside transient artifact directories. -- Add or update tests that prove the changed behavior. - -Scope: -${spec.description} - -Own only declared targets unless review feedback explicitly narrows a required fix: -${formatList(spec.targetFiles.length > 0 ? spec.targetFiles : [noTargetInstructions])} - -Acceptance gates: -${formatList(spec.acceptanceGates.map((gate) => gate.gate))} +Read these packaged context files before editing: +- ${artifactsDir}/implementation-instructions.md +- ${artifactsDir}/normalized-spec.md +- ${artifactsDir}/acceptance-contract.json +- ${artifactsDir}/deliverables.md +- ${artifactsDir}/verification-plan.md +- ${artifactsDir}/lead-plan.md +- ${artifactsDir}/matched-skills.md +${spec.targetContext ? `- ${artifactsDir}/target-context.txt` : ''} + +Own only the declared targets from ${artifactsDir}/acceptance-contract.json unless review feedback narrows a required fix. +${spec.targetFiles.length === 0 ? noTargetInstructions : `Declared target files are listed in ${artifactsDir}/deliverables.md.`} ${renderToolSelectionSummary(selection)} -Before editing, read ${artifactsDir}/matched-skills.md when it exists and use it only as generation-time context for this task. - -Keep execution routing explicit for local, cloud, and MCP callers. Materialize outputs to disk, then stop for deterministic gates. - -Generated workflow quality: -- Include a real deterministic sanity gate over produced files, not just prose saying one exists. -- Prefer POSIX grep, git grep, or a small inline assertion command that exits non-zero when expected content/state is missing. -- If using rg, guard it with command -v rg and provide a grep or git grep fallback. -- For cleanup or deletion work, persist a changed-files inventory with statuses, active-reference evidence for deleted paths, and command summaries for final signoff. -- For cleanup or deletion work, start from ${artifactsDir}/cleanup-candidate-prescan.txt and cite that exact path in ${artifactsDir}/cleanup-report.md so the evidence trail names its prescan input. -- Keep each agent step bounded to one coherent slice. Split broad implementation or test-writing work into sequential/fan-out steps with deterministic gates between them instead of relying on a single long agent timeout.`)}, +Keep execution routing explicit for local, cloud, and MCP callers. Materialize outputs to disk, then stop for deterministic gates.`)}, })`; } @@ -676,8 +842,8 @@ function renderReviewStep( stepName: string, agent: string, dependsOn: string[], - spec: NormalizedWorkflowSpec, artifactsDir: string, + hasTargetContext: boolean, selection?: ToolSelection, final = false, ): string { @@ -691,14 +857,13 @@ ${selectionLines} timeoutMs: ${DEFAULT_REVIEW_TIMEOUT_MS}, task: ${templateLiteral(`${final ? 'Re-review the fixed state only.' : 'Review the generated work.'} -Assess: -- declared file targets and non-goals -- deterministic gates and evidence quality -- review/fix/final-review 80-to-100 loop shape -- local/cloud/MCP routing clarity - -Spec: -${spec.description} +Read: +- ${artifactsDir}/review-checklist.md +- ${artifactsDir}/normalized-spec.md +- ${artifactsDir}/acceptance-contract.json +- ${artifactsDir}/lead-plan.md +- ${artifactsDir}/verification-plan.md +${hasTargetContext ? `- ${artifactsDir}/target-context.txt` : ''} ${renderToolSelectionSummary(selection)} Write ${reviewPath} ending with ${marker}.`)}, @@ -716,7 +881,7 @@ function renderSecondaryReviewStep( final = false, ): string { if (isCodeWorkflow) { - return renderReviewStep(stepName, 'reviewer-codex', dependsOn, spec, artifactsDir, selection, final); + return renderReviewStep(stepName, 'reviewer-codex', dependsOn, artifactsDir, Boolean(spec.targetContext), selection, final); } const marker = final ? 'FINAL_REVIEW_CODEX_PASS' : 'REVIEW_COMPLETE'; @@ -873,6 +1038,76 @@ function assertRickyWorkflowEnv(names: string[]): void { }`; } +function renderGeneratedContextWriterHelper(): string { + return `interface RickyGeneratedContextFile { + path: string; + content: string; +} + +interface RickyGeneratedTargetContext { + value: string; + outputPath: string; +} + +function writeRickyGeneratedContextFiles(files: RickyGeneratedContextFile[], targetContext?: RickyGeneratedTargetContext): void { + for (const file of files) { + rickyWorkflowFs.mkdirSync(rickyWorkflowPath.dirname(file.path), { recursive: true }); + rickyWorkflowFs.writeFileSync(file.path, ensureTrailingNewline(file.content)); + } + + if (!targetContext) return; + + rickyWorkflowFs.mkdirSync(rickyWorkflowPath.dirname(targetContext.outputPath), { recursive: true }); + const targetContextSourcePath = resolveRickyGeneratedTargetContextPath(targetContext.value); + if (targetContextSourcePath) { + rickyWorkflowFs.copyFileSync(targetContextSourcePath, targetContext.outputPath); + return; + } + + rickyWorkflowFs.writeFileSync(targetContext.outputPath, ensureTrailingNewline(targetContext.value)); +} + +function resolveRickyGeneratedTargetContextPath(value: string): string | null { + if (rickyWorkflowPath.isAbsolute(value)) return null; + + const workspaceRoot = rickyWorkflowFs.realpathSync(process.cwd()); + const candidatePath = rickyWorkflowPath.resolve(workspaceRoot, value); + + try { + if (!rickyWorkflowFs.existsSync(candidatePath) || !rickyWorkflowFs.statSync(candidatePath).isFile()) { + return null; + } + + const realCandidatePath = rickyWorkflowFs.realpathSync(candidatePath); + if (realCandidatePath === workspaceRoot) return null; + if (!realCandidatePath.startsWith(\`\${workspaceRoot}\${rickyWorkflowPath.sep}\`)) return null; + return realCandidatePath; + } catch { + return null; + } +} + +function ensureTrailingNewline(value: string): string { + return value.endsWith('\\n') ? value : \`\${value}\\n\`; +}`; +} + +function renderGeneratedContextWriterCall(contextSetup: GeneratedContextSetup): string { + const targetContext = contextSetup.targetContext + ? `, ${contextTargetLiteral(contextSetup.targetContext)}` + : ''; + return ` writeRickyGeneratedContextFiles(${contextFilesLiteral(contextSetup.files)}${targetContext});`; +} + +function contextFilesLiteral(files: GeneratedContextFile[]): string { + if (files.length === 0) return '[]'; + return `[\n${files.map((file) => ` { path: ${literal(file.path)}, content: ${literal(file.content)} }`).join(',\n')}\n ]`; +} + +function contextTargetLiteral(targetContext: NonNullable): string { + return `{ value: ${literal(targetContext.value)}, outputPath: ${literal(targetContext.outputPath)} }`; +} + function buildFinalArtifactConsistencyGateCommand(artifactsDir: string): string { return [ 'node <<\'NODE\'',