Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 24 additions & 3 deletions src/product/generation/master-workflow-renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,27 @@ interface RenderedMasterWorkflow {
plan: MasterExecutionPlan;
}

// Workspace-aware typecheck: prefer the project's own `npm run typecheck`
// script when one exists (the right thing for monorepos like
// `npm run typecheck -ws` or custom build pipelines), and fall back to
// `npx tsc --noEmit` when the project is a flat single-tsconfig repo.
//
// Without this guard, `npx tsc --noEmit` invoked from a monorepo root with
// no top-level tsconfig.json (npm workspaces, packages/*/tsconfig.json
// layout — common in MSD-style repos) finds neither input files nor a
// config and dumps the full `tsc --help` text on stdout while exiting 1.
// The auto-fix loop then "repairs" the workflow 7×, all failing identically
// because the workflow command is correct in general — just wrong for this
// repo shape.
//
// `npm pkg get scripts.typecheck` returns `"<command>"` when the script
// exists and `{}` when it does not (npm v7.20.0+, shipped with Node 16+).
// We compare against the literal `{}` so the snippet is portable across
// any package layout. The substring `npx tsc --noEmit` is preserved so
// downstream tools and human readers still recognize the intent.
const TYPECHECK_COMMAND =
'if [ "$(npm pkg get scripts.typecheck 2>/dev/null)" != "{}" ]; then npm run typecheck; else npx tsc --noEmit; fi';

const MASTER_EXPLICIT_PATTERN =
/\b(master executor|master orchestration|smaller workflows|child workflows|several workflows|multiple workflows|break(?:ing)? (?:it )?(?:out|up)|divvy|decompos(?:e|ition)|workflow waves?)\b/i;

Expand Down Expand Up @@ -261,7 +282,7 @@ function renderMasterSource(input: {
' dependsOn: ["review-child-evidence"],',
` command: ${literal([
'set -e',
'npx tsc --noEmit',
TYPECHECK_COMMAND,
'npm test',
'git diff --name-only',
`grep -F RICKY_MASTER_REVIEW_READY ${shellQuote(`${input.artifactsDir}/review-codex.md`)}`,
Expand Down Expand Up @@ -462,9 +483,9 @@ function buildMasterGates(artifactsDir: string, plan: MasterExecutionPlan): Dete
gate('skill-boundary-metadata-gate', `test -f ${artifactsDir}/skill-application-boundary.json`, 'file_exists', true, ['prepare-context'], 'pre_review'),
gate('lead-plan-gate', `grep -F RICKY_MASTER_LEAD_PLAN_READY ${artifactsDir}/lead-plan.md`, 'output_contains', true, ['lead-plan'], 'pre_review'),
gate('child-workflow-file-gate', plan.children.map((child) => `test -f ${child.workflowFilePath}`).join(' && '), 'file_exists', true, ['materialize-child-workflows'], 'pre_review'),
gate('initial-soft-validation', 'npx tsc --noEmit 2>&1 | tail -160', 'output_contains', false, ['child-workflow-file-gate'], 'pre_review'),
gate('initial-soft-validation', `{ ${TYPECHECK_COMMAND}; } 2>&1 | tail -160`, 'output_contains', false, ['child-workflow-file-gate'], 'pre_review'),
gate('final-review-pass-gate', `grep -F RICKY_MASTER_REVIEW_READY ${artifactsDir}/review-codex.md`, 'output_contains', true, ['review-child-evidence'], 'final'),
gate('final-hard-validation', 'npx tsc --noEmit && npm test', 'exit_code', true, ['final-review-pass-gate'], 'final'),
gate('final-hard-validation', `{ ${TYPECHECK_COMMAND}; } && npm test`, 'exit_code', true, ['final-review-pass-gate'], 'final'),
gate('git-diff-gate', 'git diff --name-only', 'output_contains', true, ['final-hard-validation'], 'final'),
gate('regression-gate', 'npm test', 'exit_code', true, ['git-diff-gate'], 'regression'),
];
Expand Down
37 changes: 37 additions & 0 deletions src/product/generation/pipeline.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,43 @@ describe('workflow generation pipeline', () => {
expect(rendered.content).toContain('.run({ cwd: process.cwd() })');
});

// Regression: master-rendered final-hard-validation used to hardcode
// `npx tsc --noEmit`, which dumps the full `tsc --help` text and exits 1
// when invoked from a monorepo root with no top-level tsconfig.json
// (npm workspaces with `packages/*/tsconfig.json` layout — common in
// MSD-style repos). The auto-fix loop then "repaired" the workflow 7×,
// all failing identically because the workflow command was correct in
// general — just wrong for that repo shape. The renderer now emits a
// workspace-aware shell snippet that prefers `npm run typecheck` when the
// project defines that script and falls back to `npx tsc --noEmit`
// otherwise. The fallback path keeps `npx tsc --noEmit` as a literal
// substring so downstream tests, evidence capture, and human readers
// still recognize the intent.
it('emits a workspace-aware typecheck command in master-rendered final-hard-validation', () => {
const result = generate({
spec: spec({
description:
'Implement nested runner, runtime policy, telemetry, evals, and insights as smaller workflows run by a master executor.',
constraints: ['Use independent child workflows with deterministic 80-to-100 validation.'],
acceptanceGates: ['npm test'],
}),
artifactPath: 'workflows/generated/runtime-master.ts',
});
expect(result.masterExecutionPlan).toBeDefined();
const rendered = artifact(result).content;

// The final-hard-validation step body must include both branches of the
// workspace-aware fallback so monorepos and flat repos both succeed.
expect(rendered).toContain('npm pkg get scripts.typecheck');
expect(rendered).toContain('npm run typecheck');
expect(rendered).toContain('npx tsc --noEmit');

// The bare `npx tsc --noEmit` (without the conditional guard) must not
// appear as the first command after `set -e` in any rendered .step body.
// That pattern is what would dump the tsc help text in monorepo roots.
expect(rendered).not.toMatch(/command: "set -e\\nnpx tsc --noEmit\\n/);
});

it('uses a master workflow for broad target-file specs and leaves narrow specs on the existing renderer', () => {
const broad = generate({
spec: spec({
Expand Down
Loading