From d3a27b34cedad5b696b00b39fca47b916942b060 Mon Sep 17 00:00:00 2001 From: Salman Ansari Date: Mon, 18 May 2026 15:59:13 -0700 Subject: [PATCH 1/4] feat(review): add review_depth: lightweight to suppress preamble for low-risk steps Adds a `review_depth: lightweight` annotation to review blocks in job.yml step outputs and .deepreview rules. When set, the workflow's common_job_info preamble (## Job Context) is omitted from review instruction files, reducing token overhead for trivial or reversible intermediate steps. Step inputs and review criteria are always included regardless of depth. - Parser: ReviewBlock gains review_depth field - Config: ReviewRule and ReviewTask carry review_depth through the pipeline - Matcher: review_depth threaded from rule to task for all three strategies - QualityGate: _build_preamble() conditionally omits Job Context when lightweight - Schemas: review_depth enum ["lightweight"] added to job.schema.json and deepreview_schema.json - Tests: 1299 passing, coverage at 98.07% Closes #86 Co-Authored-By: Claude Sonnet 4.6 --- src/deepwork/jobs/job.schema.json | 5 + src/deepwork/jobs/mcp/quality_gate.py | 29 +++- src/deepwork/jobs/parser.py | 2 + src/deepwork/review/config.py | 5 + src/deepwork/review/matcher.py | 3 + src/deepwork/schemas/deepreview_schema.json | 5 + tests/unit/jobs/mcp/test_quality_gate.py | 182 ++++++++++++++++++++ tests/unit/jobs/test_parser.py | 25 +++ tests/unit/review/test_matcher.py | 106 ++++++++++++ 9 files changed, 355 insertions(+), 7 deletions(-) diff --git a/src/deepwork/jobs/job.schema.json b/src/deepwork/jobs/job.schema.json index f8b7320e..4f7d60db 100644 --- a/src/deepwork/jobs/job.schema.json +++ b/src/deepwork/jobs/job.schema.json @@ -120,6 +120,11 @@ "description": "If true, includes matching files that were not produced as outputs but exist on disk. Useful for document freshness reviews where the reviewer needs to see the doc even when only source files changed." } } + }, + "review_depth": { + "type": "string", + "enum": ["lightweight"], + "description": "Optional review depth hint. When set to 'lightweight', the workflow's common_job_info preamble is omitted from the review instruction file, reducing token usage for low-risk or reversible intermediate steps. Step inputs and review criteria are always included. Omit this field for standard depth (default)." } } }, diff --git a/src/deepwork/jobs/mcp/quality_gate.py b/src/deepwork/jobs/mcp/quality_gate.py index ba1de5cd..7178e44e 100644 --- a/src/deepwork/jobs/mcp/quality_gate.py +++ b/src/deepwork/jobs/mcp/quality_gate.py @@ -91,17 +91,23 @@ def _build_preamble( job: JobDefinition, workflow: Workflow, input_values: dict[str, ArgumentValue], + review_depth: str | None = None, ) -> str: """Build the preamble prefixed to every dynamic review's instructions. Combines workflow ``common_job_info`` and the rendered step inputs. Returns an empty string when neither is available. + + When ``review_depth`` is ``"lightweight"``, the ``## Job Context`` block + (common_job_info) is omitted to reduce token usage for low-risk steps. + Step inputs are always included regardless of depth. """ input_context = _build_input_context(step, job, input_values) - common_info = workflow.common_job_info or "" preamble_parts: list[str] = [] - if common_info: - preamble_parts.append(f"## Job Context\n\n{common_info}") + if review_depth != "lightweight": + common_info = workflow.common_job_info or "" + if common_info: + preamble_parts.append(f"## Job Context\n\n{common_info}") if input_context: preamble_parts.append(input_context) return "\n\n".join(preamble_parts) @@ -160,7 +166,6 @@ def build_dynamic_review_rules( targets. """ rules: list[ReviewRule] = [] - preamble = _build_preamble(step, job, workflow, input_values) # Process each output for output_name, output_ref in step.outputs.items(): @@ -191,6 +196,11 @@ def build_dynamic_review_rules( file_paths = [] for i, review_block in enumerate(review_blocks): + # Build preamble, respecting review_depth on this block + preamble = _build_preamble( + step, job, workflow, input_values, review_block.review_depth + ) + # Build full instructions with preamble full_instructions = ( f"{preamble}\n\n{review_block.instructions}" @@ -225,10 +235,11 @@ def build_dynamic_review_rules( source_dir=project_root, source_file=job.job_dir / "job.yml", source_line=0, + review_depth=review_block.review_depth, ) rules.append(rule) - # Process requirements review + # Process requirements review — always uses standard preamble (no review_depth suppression) if step.process_requirements and work_summary is not None: attrs_list = "\n".join( f"- **{name}**: {statement}" for name, statement in step.process_requirements.items() @@ -249,7 +260,8 @@ def build_dynamic_review_rules( output_context = "\n".join(output_context_parts) - pqa_instructions = f"""{preamble} + pqa_preamble = _build_preamble(step, job, workflow, input_values) + pqa_instructions = f"""{pqa_preamble} ## Process Requirements Review @@ -315,7 +327,6 @@ def build_string_output_review_tasks( ``_arg`` to distinguish it (matching the file_path rule naming). """ tasks: list[ReviewTask] = [] - preamble = _build_preamble(step, job, workflow, input_values) try: source_rel = (job.job_dir / "job.yml").relative_to(project_root) @@ -345,6 +356,9 @@ def build_string_output_review_tasks( inline_value = value if isinstance(value, str) else str(value) for i, review_block in enumerate(review_blocks): + preamble = _build_preamble( + step, job, workflow, input_values, review_block.review_depth + ) full_instructions = ( f"{preamble}\n\n{review_block.instructions}" if preamble @@ -364,6 +378,7 @@ def build_string_output_review_tasks( agent_name=agent_name, source_location=source_location, inline_content=inline_value, + review_depth=review_block.review_depth, ) ) diff --git a/src/deepwork/jobs/parser.py b/src/deepwork/jobs/parser.py index 8b15625c..571d90f2 100644 --- a/src/deepwork/jobs/parser.py +++ b/src/deepwork/jobs/parser.py @@ -26,6 +26,7 @@ class ReviewBlock: instructions: str agent: dict[str, str] | None = None additional_context: dict[str, bool] | None = None + review_depth: str | None = None # "lightweight" | None (standard) @classmethod def from_dict(cls, data: dict[str, Any]) -> "ReviewBlock": @@ -35,6 +36,7 @@ def from_dict(cls, data: dict[str, Any]) -> "ReviewBlock": instructions=data["instructions"], agent=data.get("agent"), additional_context=data.get("additional_context"), + review_depth=data.get("review_depth"), ) diff --git a/src/deepwork/review/config.py b/src/deepwork/review/config.py index 5909e9a8..eda49ce5 100644 --- a/src/deepwork/review/config.py +++ b/src/deepwork/review/config.py @@ -49,6 +49,7 @@ class ReviewRule: source_file: Path # Path to the .deepreview file source_line: int # Line number of the rule name in the .deepreview file reference_files: list[ReferenceFile] = field(default_factory=list) + review_depth: str | None = None # "lightweight" | None (standard) @dataclass @@ -65,6 +66,7 @@ class ReviewTask: precomputed_info_bash_command: str | None = None # Resolved command to run inline_content: str | None = None # Inline string value for type: string outputs reference_files: list[ReferenceFile] = field(default_factory=list) + review_depth: str | None = None # "lightweight" | None (standard) def parse_deepreview_file(filepath: Path) -> list[ReviewRule]: @@ -150,6 +152,8 @@ def _parse_rule( reference_files = _parse_reference_files(review_data.get("reference_files", []), source_dir) + review_depth = review_data.get("review_depth") + return ReviewRule( name=name, description=description, @@ -165,6 +169,7 @@ def _parse_rule( source_file=source_file, source_line=source_line, reference_files=reference_files, + review_depth=review_depth, ) diff --git a/src/deepwork/review/matcher.py b/src/deepwork/review/matcher.py index 5832e79d..5c0d9a68 100644 --- a/src/deepwork/review/matcher.py +++ b/src/deepwork/review/matcher.py @@ -246,6 +246,7 @@ def match_files_to_rules( all_changed_filenames=all_filenames, precomputed_info_bash_command=precompute_cmd, reference_files=task_refs, + review_depth=rule.review_depth, ) ) @@ -264,6 +265,7 @@ def match_files_to_rules( all_changed_filenames=all_filenames, precomputed_info_bash_command=precompute_cmd, reference_files=rule.reference_files, + review_depth=rule.review_depth, ) ) @@ -278,6 +280,7 @@ def match_files_to_rules( all_changed_filenames=all_filenames, precomputed_info_bash_command=precompute_cmd, reference_files=rule.reference_files, + review_depth=rule.review_depth, ) ) diff --git a/src/deepwork/schemas/deepreview_schema.json b/src/deepwork/schemas/deepreview_schema.json index a472b769..057440f1 100644 --- a/src/deepwork/schemas/deepreview_schema.json +++ b/src/deepwork/schemas/deepreview_schema.json @@ -136,6 +136,11 @@ "description": "If true, pulls the full contents of files that match the include patterns even if they weren't modified in this diff." } } + }, + "review_depth": { + "type": "string", + "enum": ["lightweight"], + "description": "Optional review depth hint. When set to 'lightweight', the workflow's common_job_info preamble is omitted from the review instruction file, reducing token usage for low-risk or reversible intermediate steps. Omit for standard depth (default)." } } } diff --git a/tests/unit/jobs/mcp/test_quality_gate.py b/tests/unit/jobs/mcp/test_quality_gate.py index 32a38540..b7dc3b1b 100644 --- a/tests/unit/jobs/mcp/test_quality_gate.py +++ b/tests/unit/jobs/mcp/test_quality_gate.py @@ -1029,6 +1029,20 @@ def test_renders_file_path_single_input(self, tmp_path: Path) -> None: result = _build_input_context(step, job, {"ref": "report.md"}) assert "@report.md" in result + def test_renders_not_available_when_input_value_missing(self, tmp_path: Path) -> None: + """When a known input has no value in input_values, shows '— *not available*'.""" + from deepwork.jobs.mcp.quality_gate import _build_input_context + + arg = StepArgument(name="report", description="The report file", type="file_path") + input_ref = StepInputRef(argument_name="report", required=False) + step = WorkflowStep(name="s", inputs={"report": input_ref}, outputs={}) + job, _ = _make_job(tmp_path, [arg], step) + + # input_values is empty — "report" is declared but not available + result = _build_input_context(step, job, {}) + assert "report" in result + assert "not available" in result + # --------------------------------------------------------------------------- # TestBuildDynamicReviewRules — additional coverage @@ -1133,6 +1147,34 @@ def test_process_requirements_with_list_file_and_string_outputs(self, tmp_path: assert "@b.md" in rule.instructions assert "all good" in rule.instructions + def test_process_requirements_string_output_before_file_output(self, tmp_path: Path) -> None: + """String output rendered before file_path output in process requirements context.""" + str_arg = StepArgument(name="note", description="Note", type="string") + file_arg = StepArgument(name="report", description="Report", type="file_path") + str_ref = StepOutputRef(argument_name="note", required=True) + file_ref = StepOutputRef(argument_name="report", required=True) + step = WorkflowStep( + name="analyze", + outputs={"note": str_ref, "report": file_ref}, + process_requirements={"done": "Work MUST be complete."}, + ) + job, workflow = _make_job(tmp_path, [str_arg, file_arg], step) + + rules = build_dynamic_review_rules( + step=step, + job=job, + workflow=workflow, + outputs={"note": "summary text", "report": "report.md"}, + input_values={}, + work_summary="Done", + project_root=tmp_path, + ) + + assert len(rules) == 1 + rule = rules[0] + assert "summary text" in rule.instructions + assert "@report.md" in rule.instructions + def test_process_requirements_skipped_when_no_output_paths(self, tmp_path: Path) -> None: """When there are no file_path outputs, PQA rule is not created.""" str_arg = StepArgument(name="note", description="Note", type="string") @@ -1156,6 +1198,146 @@ def test_process_requirements_skipped_when_no_output_paths(self, tmp_path: Path) assert rules == [] +# --------------------------------------------------------------------------- +# TestReviewDepthLightweight +# --------------------------------------------------------------------------- + + +class TestReviewDepthLightweight: + """Tests for review_depth: lightweight — preamble suppression.""" + + # THIS TEST VALIDATES A HARD REQUIREMENT (JOBS-REQ-004.8). + # YOU MUST NOT MODIFY THIS TEST UNLESS THE REQUIREMENT CHANGES + def test_lightweight_review_omits_common_job_info(self, tmp_path: Path) -> None: + """review_depth: lightweight suppresses the ## Job Context preamble block.""" + review = ReviewBlock( + strategy="individual", + instructions="Check structural validity.", + review_depth="lightweight", + ) + arg = StepArgument(name="config", description="Config file", type="file_path", review=review) + output_ref = StepOutputRef(argument_name="config", required=True) + step = WorkflowStep(name="prepare", outputs={"config": output_ref}) + workflow = Workflow( + name="main", + summary="Test", + steps=[step], + common_job_info="This is expensive common job info that should be suppressed.", + ) + job = JobDefinition( + name="test_job", + summary="Test job", + step_arguments=[arg], + workflows={"main": workflow}, + job_dir=tmp_path / ".deepwork" / "jobs" / "test_job", + ) + job.job_dir.mkdir(parents=True, exist_ok=True) + + rules = build_dynamic_review_rules( + step=step, + job=job, + workflow=workflow, + outputs={"config": "config.yml"}, + input_values={}, + work_summary=None, + project_root=tmp_path, + ) + + assert len(rules) == 1 + assert "expensive common job info" not in rules[0].instructions + assert "Check structural validity." in rules[0].instructions + assert rules[0].review_depth == "lightweight" + + # THIS TEST VALIDATES A HARD REQUIREMENT (JOBS-REQ-004.8). + # YOU MUST NOT MODIFY THIS TEST UNLESS THE REQUIREMENT CHANGES + def test_standard_review_includes_common_job_info(self, tmp_path: Path) -> None: + """Without review_depth, common_job_info is included in the review preamble.""" + review = ReviewBlock( + strategy="individual", + instructions="Check carefully.", + ) + arg = StepArgument(name="report", description="Report", type="file_path", review=review) + output_ref = StepOutputRef(argument_name="report", required=True) + step = WorkflowStep(name="analyze", outputs={"report": output_ref}) + workflow = Workflow( + name="main", + summary="Test", + steps=[step], + common_job_info="This is important job context.", + ) + job = JobDefinition( + name="test_job", + summary="Test job", + step_arguments=[arg], + workflows={"main": workflow}, + job_dir=tmp_path / ".deepwork" / "jobs" / "test_job", + ) + job.job_dir.mkdir(parents=True, exist_ok=True) + + rules = build_dynamic_review_rules( + step=step, + job=job, + workflow=workflow, + outputs={"report": "report.md"}, + input_values={}, + work_summary=None, + project_root=tmp_path, + ) + + assert len(rules) == 1 + assert "This is important job context." in rules[0].instructions + assert rules[0].review_depth is None + + # THIS TEST VALIDATES A HARD REQUIREMENT (JOBS-REQ-004.8). + # YOU MUST NOT MODIFY THIS TEST UNLESS THE REQUIREMENT CHANGES + def test_lightweight_still_includes_step_inputs(self, tmp_path: Path) -> None: + """Lightweight reviews still include step input context — only Job Context is suppressed.""" + review = ReviewBlock( + strategy="individual", + instructions="Check it.", + review_depth="lightweight", + ) + scope_arg = StepArgument(name="scope", description="Scope doc", type="file_path") + config_arg = StepArgument( + name="config", description="Config file", type="file_path", review=review + ) + scope_ref = StepOutputRef(argument_name="scope", required=True) + config_ref = StepOutputRef(argument_name="config", required=True) + step = WorkflowStep( + name="prepare", + inputs={"scope": StepInputRef(argument_name="scope", required=True)}, + outputs={"config": config_ref, "scope": scope_ref}, + ) + workflow = Workflow( + name="main", + summary="Test", + steps=[step], + common_job_info="Suppressed common info.", + ) + job = JobDefinition( + name="test_job", + summary="Test job", + step_arguments=[scope_arg, config_arg], + workflows={"main": workflow}, + job_dir=tmp_path / ".deepwork" / "jobs" / "test_job", + ) + job.job_dir.mkdir(parents=True, exist_ok=True) + + rules = build_dynamic_review_rules( + step=step, + job=job, + workflow=workflow, + outputs={"config": "config.yml", "scope": "scope.md"}, + input_values={"scope": "scope.md"}, + work_summary=None, + project_root=tmp_path, + ) + + config_rule = next(r for r in rules if "config" in r.name) + assert "Suppressed common info." not in config_rule.instructions + assert "Step Inputs" in config_rule.instructions + + # --------------------------------------------------------------------------- # TestRunQualityGate — additional coverage # --------------------------------------------------------------------------- diff --git a/tests/unit/jobs/test_parser.py b/tests/unit/jobs/test_parser.py index b9d527e3..bc3698a0 100644 --- a/tests/unit/jobs/test_parser.py +++ b/tests/unit/jobs/test_parser.py @@ -59,6 +59,31 @@ def test_from_dict_minimal(self) -> None: assert review.agent is None assert review.additional_context is None + def test_from_dict_with_review_depth_lightweight(self) -> None: + """review_depth: lightweight is parsed and stored on the ReviewBlock.""" + # THIS TEST VALIDATES A HARD REQUIREMENT (JOBS-REQ-002.4.6). + # YOU MUST NOT MODIFY THIS TEST UNLESS THE REQUIREMENT CHANGES + data = { + "strategy": "individual", + "instructions": "Check structural validity only.", + "review_depth": "lightweight", + } + review = ReviewBlock.from_dict(data) + + assert review.review_depth == "lightweight" + + def test_from_dict_review_depth_defaults_to_none(self) -> None: + """review_depth is None when not specified (standard depth, backward-compatible).""" + # THIS TEST VALIDATES A HARD REQUIREMENT (JOBS-REQ-002.4.6). + # YOU MUST NOT MODIFY THIS TEST UNLESS THE REQUIREMENT CHANGES + data = { + "strategy": "individual", + "instructions": "Review this carefully.", + } + review = ReviewBlock.from_dict(data) + + assert review.review_depth is None + class TestStepArgument: """Tests for StepArgument dataclass.""" diff --git a/tests/unit/review/test_matcher.py b/tests/unit/review/test_matcher.py index 94c2f5a5..345bdb47 100644 --- a/tests/unit/review/test_matcher.py +++ b/tests/unit/review/test_matcher.py @@ -906,6 +906,84 @@ def test_exclude_patterns_filter_unchanged_files(self, tmp_path: Path) -> None: assert "app.py" in result assert "test_app.py" not in result + def test_skips_glob_match_not_relative_to_project(self, tmp_path: Path) -> None: + # THIS TEST VALIDATES A HARD REQUIREMENT (REVIEW-REQ-004.4.5). + # YOU MUST NOT MODIFY THIS TEST UNLESS THE REQUIREMENT CHANGES + """Glob matches whose path cannot be made relative to project_root are skipped.""" + from unittest.mock import MagicMock + + from deepwork.review.config import ReviewRule + from deepwork.review.matcher import _find_unchanged_matching_files + + # Use a mock source_dir so we can control glob() output. + # relative_to(project_root) must succeed (source_dir IS under project_root). + mock_source = MagicMock(spec=Path) + mock_source.relative_to.return_value = Path("src") + + # Glob returns a path whose relative_to(project_root) raises ValueError. + outside_path = MagicMock(spec=Path) + outside_path.is_file.return_value = True + outside_path.relative_to.side_effect = ValueError("not under project root") + mock_source.glob.return_value = [outside_path] + + rule = ReviewRule( + name="test", + description="test", + include_patterns=["*.py"], + exclude_patterns=[], + strategy="individual", + instructions="test", + agent=None, + all_changed_filenames=False, + unchanged_matching_files=False, + precomputed_info_bash_command=None, + source_dir=mock_source, + source_file=tmp_path / ".deepreview", + source_line=1, + ) + + result = _find_unchanged_matching_files([], rule, tmp_path) + assert result == [] + + def test_skips_glob_match_not_relative_to_source_dir(self, tmp_path: Path) -> None: + # THIS TEST VALIDATES A HARD REQUIREMENT (REVIEW-REQ-004.4.5). + # YOU MUST NOT MODIFY THIS TEST UNLESS THE REQUIREMENT CHANGES + """Glob matches relative to project_root but not to source_dir are skipped.""" + from unittest.mock import MagicMock + + from deepwork.review.config import ReviewRule + from deepwork.review.matcher import _find_unchanged_matching_files + + # source_dir mock reports itself as "src" relative to project_root. + mock_source = MagicMock(spec=Path) + mock_source.relative_to.return_value = Path("src") + + # Glob returns a path under project_root but NOT under src/. + sibling_mock = MagicMock(spec=Path) + sibling_mock.is_file.return_value = True + sibling_mock.relative_to.return_value = Path("other/file.py") + mock_source.glob.return_value = [sibling_mock] + + rule = ReviewRule( + name="test", + description="test", + include_patterns=["*.py"], + exclude_patterns=[], + strategy="individual", + instructions="test", + agent=None, + all_changed_filenames=False, + unchanged_matching_files=False, + precomputed_info_bash_command=None, + source_dir=mock_source, + source_file=tmp_path / ".deepreview", + source_line=1, + ) + + result = _find_unchanged_matching_files([], rule, tmp_path) + # "other/file.py" is not relative to "src" → _relative_to_dir returns None → skipped + assert "other/file.py" not in result + class TestMatchFilesToRulesAllChangedFiles: """Test the all_changed_files strategy branch more thoroughly.""" @@ -929,6 +1007,34 @@ def test_all_changed_files_strategy_with_agent(self, tmp_path: Path) -> None: assert set(tasks[0].files_to_review) == {"app.py", "lib.py", "main.ts"} assert tasks[0].agent_name == "reviewer" + def test_all_changed_files_strategy_with_subsequent_rule(self, tmp_path: Path) -> None: + # THIS TEST VALIDATES A HARD REQUIREMENT (REVIEW-REQ-004.5.1, REVIEW-REQ-004.9.2). + # YOU MUST NOT MODIFY THIS TEST UNLESS THE REQUIREMENT CHANGES + """all_changed_files rule followed by another rule — both produce tasks.""" + rule_acf = _make_rule( + name="acf_rule", + strategy="all_changed_files", + source_dir=tmp_path, + ) + rule_ind = _make_rule( + name="ind_rule", + strategy="individual", + include=["**/*.py"], + source_dir=tmp_path, + ) + tasks = match_files_to_rules( + ["app.py", "main.ts"], + [rule_acf, rule_ind], + tmp_path, + ) + # First rule: all_changed_files → 1 task with all files + # Second rule: individual → 1 task (only app.py matches *.py) + assert len(tasks) == 2 + acf_task = next(t for t in tasks if t.rule_name == "acf_rule") + ind_task = next(t for t in tasks if t.rule_name == "ind_rule") + assert set(acf_task.files_to_review) == {"app.py", "main.ts"} + assert ind_task.files_to_review == ["app.py"] + class TestPrecomputedInfoThreading: """Tests that precomputed_info_bash_command threads from rule to task.""" From 0adf625e1f6558c1daf84488567f8624777253f8 Mon Sep 17 00:00:00 2001 From: Salman Ansari Date: Mon, 18 May 2026 16:47:21 -0700 Subject: [PATCH 2/4] docs: add changelog entry for review_depth: lightweight Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7444d617..f7f09f0e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- `review_depth: lightweight` annotation for review blocks in `job.yml` step outputs and `.deepreview` rules — when set, the workflow's `common_job_info` preamble is omitted from review instruction files, reducing token overhead for trivial or reversible intermediate steps (closes #86) + ### Changed ### Fixed From 5b77d56ac93a31edc982dc1d0a2bde77f8c9c826 Mon Sep 17 00:00:00 2001 From: Salman Ansari Date: Mon, 18 May 2026 17:19:26 -0700 Subject: [PATCH 3/4] fix(review): clarify review_depth no-op in .deepreview and add string-output test - deepreview_schema.json: note that review_depth has no effect in .deepreview rules since they are not associated with a workflow and receive no common_job_info - test_quality_gate.py: add test for build_string_output_review_tasks with review_depth: lightweight to confirm common_job_info is suppressed on string outputs Co-Authored-By: Claude Sonnet 4.6 --- src/deepwork/schemas/deepreview_schema.json | 2 +- tests/unit/jobs/mcp/test_quality_gate.py | 42 +++++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/src/deepwork/schemas/deepreview_schema.json b/src/deepwork/schemas/deepreview_schema.json index 057440f1..b4b062f6 100644 --- a/src/deepwork/schemas/deepreview_schema.json +++ b/src/deepwork/schemas/deepreview_schema.json @@ -140,7 +140,7 @@ "review_depth": { "type": "string", "enum": ["lightweight"], - "description": "Optional review depth hint. When set to 'lightweight', the workflow's common_job_info preamble is omitted from the review instruction file, reducing token usage for low-risk or reversible intermediate steps. Omit for standard depth (default)." + "description": "Optional review depth hint. When set to 'lightweight' on a job.yml review block, the workflow's common_job_info preamble is omitted from the review instruction file, reducing token usage for low-risk or reversible intermediate steps. Note: this field has no effect in .deepreview rules — those rules are not associated with a workflow and do not receive common_job_info. Omit for standard depth (default)." } } } diff --git a/tests/unit/jobs/mcp/test_quality_gate.py b/tests/unit/jobs/mcp/test_quality_gate.py index b7dc3b1b..c5b2bb90 100644 --- a/tests/unit/jobs/mcp/test_quality_gate.py +++ b/tests/unit/jobs/mcp/test_quality_gate.py @@ -1599,6 +1599,48 @@ def test_job_dir_outside_project_root_falls_back(self, tmp_path: Path) -> None: assert str(outside) in tasks[0].source_location assert tasks[0].source_location.endswith("job.yml:0") + # THIS TEST VALIDATES A HARD REQUIREMENT (JOBS-REQ-004.8). + # YOU MUST NOT MODIFY THIS TEST UNLESS THE REQUIREMENT CHANGES + def test_lightweight_review_depth_omits_common_job_info(self, tmp_path: Path) -> None: + """review_depth: lightweight suppresses common_job_info in string-output review tasks.""" + review = ReviewBlock( + strategy="individual", + instructions="Verify the summary is accurate.", + review_depth="lightweight", + ) + arg = StepArgument(name="summary", description="Summary", type="string") + output_ref = StepOutputRef(argument_name="summary", required=True, review=review) + step = WorkflowStep(name="analyze", outputs={"summary": output_ref}) + workflow = Workflow( + name="main", + summary="Test", + steps=[step], + common_job_info="Expensive job context that should be suppressed.", + ) + job = JobDefinition( + name="test_job", + summary="Test job", + step_arguments=[arg], + workflows={"main": workflow}, + job_dir=tmp_path / ".deepwork" / "jobs" / "test_job", + ) + job.job_dir.mkdir(parents=True, exist_ok=True) + + tasks = build_string_output_review_tasks( + step=step, + job=job, + workflow=workflow, + outputs={"summary": "The result was X."}, + input_values={}, + project_root=tmp_path, + ) + + assert len(tasks) == 1 + task = tasks[0] + assert "Expensive job context" not in task.instructions + assert "Verify the summary is accurate." in task.instructions + assert task.review_depth == "lightweight" + class TestRunQualityGateStringOutputs: """End-to-end tests: run_quality_gate emits reviews for string outputs. From bb9b6532974eddf1140a34ce7eef8bde8e83ad83 Mon Sep 17 00:00:00 2001 From: Salman Ansari Date: Mon, 18 May 2026 17:51:58 -0700 Subject: [PATCH 4/4] style: apply ruff formatting to quality_gate and test_quality_gate --- src/deepwork/jobs/mcp/quality_gate.py | 8 ++------ tests/unit/jobs/mcp/test_quality_gate.py | 4 +++- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/deepwork/jobs/mcp/quality_gate.py b/src/deepwork/jobs/mcp/quality_gate.py index 7178e44e..3a23ca5f 100644 --- a/src/deepwork/jobs/mcp/quality_gate.py +++ b/src/deepwork/jobs/mcp/quality_gate.py @@ -197,9 +197,7 @@ def build_dynamic_review_rules( for i, review_block in enumerate(review_blocks): # Build preamble, respecting review_depth on this block - preamble = _build_preamble( - step, job, workflow, input_values, review_block.review_depth - ) + preamble = _build_preamble(step, job, workflow, input_values, review_block.review_depth) # Build full instructions with preamble full_instructions = ( @@ -356,9 +354,7 @@ def build_string_output_review_tasks( inline_value = value if isinstance(value, str) else str(value) for i, review_block in enumerate(review_blocks): - preamble = _build_preamble( - step, job, workflow, input_values, review_block.review_depth - ) + preamble = _build_preamble(step, job, workflow, input_values, review_block.review_depth) full_instructions = ( f"{preamble}\n\n{review_block.instructions}" if preamble diff --git a/tests/unit/jobs/mcp/test_quality_gate.py b/tests/unit/jobs/mcp/test_quality_gate.py index c5b2bb90..197e33e2 100644 --- a/tests/unit/jobs/mcp/test_quality_gate.py +++ b/tests/unit/jobs/mcp/test_quality_gate.py @@ -1215,7 +1215,9 @@ def test_lightweight_review_omits_common_job_info(self, tmp_path: Path) -> None: instructions="Check structural validity.", review_depth="lightweight", ) - arg = StepArgument(name="config", description="Config file", type="file_path", review=review) + arg = StepArgument( + name="config", description="Config file", type="file_path", review=review + ) output_ref = StepOutputRef(argument_name="config", required=True) step = WorkflowStep(name="prepare", outputs={"config": output_ref}) workflow = Workflow(