This is an automated email from the ASF dual-hosted git repository.
terrymanu pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/shardingsphere.git
The following commit(s) were added to refs/heads/master by this push:
new 34e9a351dc5 Enforce code handoff review output (#39834)
34e9a351dc5 is described below
commit 34e9a351dc543b4ad7efc01093b9232bfca8de1c
Author: Liang Zhang <[email protected]>
AuthorDate: Sun Sep 13 21:33:34 2026 +0800
Enforce code handoff review output (#39834)
---
.codex/config.toml | 31 +++++
.codex/context/change-completion.md | 2 +-
.codex/harness/agents/cases.toml | 7 +-
.codex/harness/agents/code_handoff_hook.py | 133 +++++++++++++++++++++
.codex/harness/agents/policy-sources.toml | 21 ++++
.codex/harness/agents/test_code_handoff_hook.py | 113 +++++++++++++++++
.../EncryptProjectionTokenGenerator.java | 2 +-
.../OracleProjectionIdentifierExtractorTest.java | 1 +
8 files changed, 304 insertions(+), 6 deletions(-)
diff --git a/.codex/config.toml b/.codex/config.toml
new file mode 100644
index 00000000000..d8ddb264778
--- /dev/null
+++ b/.codex/config.toml
@@ -0,0 +1,31 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+[[hooks.PostToolUse]]
+matcher = "^apply_patch$"
+
+[[hooks.PostToolUse.hooks]]
+type = "command"
+command = '/usr/bin/python3 "$(git rev-parse
--show-toplevel)/.codex/harness/agents/code_handoff_hook.py"'
+command_windows = 'py -3 "$(git rev-parse
--show-toplevel)/.codex/harness/agents/code_handoff_hook.py"'
+timeout = 5
+
+[[hooks.Stop]]
+
+[[hooks.Stop.hooks]]
+type = "command"
+command = '/usr/bin/python3 "$(git rev-parse
--show-toplevel)/.codex/harness/agents/code_handoff_hook.py"'
+command_windows = 'py -3 "$(git rev-parse
--show-toplevel)/.codex/harness/agents/code_handoff_hook.py"'
+timeout = 5
diff --git a/.codex/context/change-completion.md
b/.codex/context/change-completion.md
index 52432cd97f9..932352c5120 100644
--- a/.codex/context/change-completion.md
+++ b/.codex/context/change-completion.md
@@ -34,4 +34,4 @@ When the delta audit, required current-state checks, and
applicable review pass
If a report or verdict is disproved, fix the highest-leverage rule, schema,
validator, prompt, or regression case before correcting the artifact, unless
the user explicitly requests a one-off correction.
-The final response must lead with the outcome and include changed files and
rationale, verification status, remaining risks, and the next action only when
one is still required. Every code-writing handoff must explicitly report the
completed code review result and review recommendations. State the blocking
issue count and required changes, and explicitly state `No changes recommended`
when no findings exist. Do not omit the review output merely because the review
was performed internally [...]
+The final response must lead with the outcome and include changed files and
rationale, verification status, remaining risks, and the next action only when
one is still required. Every code-writing handoff must explicitly report the
completed code review result and review recommendations. Put a value on
separate `Review Result:`, `Blocking Issues:`, `Required Changes:`, and `Review
Recommendations:` lines so the project Stop hook can validate the handoff after
`apply_patch` changes a non- [...]
diff --git a/.codex/harness/agents/cases.toml b/.codex/harness/agents/cases.toml
index 53a9d251d67..9bb812981b6 100644
--- a/.codex/harness/agents/cases.toml
+++ b/.codex/harness/agents/cases.toml
@@ -1863,14 +1863,13 @@ profile = "code-write"
semantic_policy_assertions = true
prompt = "A routine authorized code task is complete. Its implementation,
applicable verification, and bounded pre-handoff self-review are finished; the
review found no required issues, and no Formal Review trigger applies. Deliver
the final handoff. A response that only says the implementation is complete,
tests passed, `Mergeable`, or gives a commit message is insufficient."
decision = "proceed"
-required_actions = []
+required_actions = ["propose_commit_message"]
allowed_actions = ["propose_commit_message"]
forbidden_actions = ["edit_code", "edit_non_code"]
required_reasons = []
-required_summary_terms = ["No changes recommended"]
-required_summary_term_groups = [["Review Result", "review passed", "review
result"], ["Blocking Issues: 0", "0 blocking issues"]]
+required_summary_terms = ["Review Result:", "Blocking Issues: 0", "Required
Changes:", "Review Recommendations:", "No changes recommended", "Commit
Message:"]
policy_binding_profile = "code-write"
-policy_assertions = [{source = "change-completion", statements = ["Every
code-writing handoff must explicitly report the completed code review result
and review recommendations.", "State the blocking issue count and required
changes, and explicitly state `No changes recommended` when no findings
exist.", "Do not omit the review output merely because the review was performed
internally.", "Validation results, passing tests, a bare `Review Result:
Mergeable`, or commit artifacts do not sub [...]
+policy_assertions = [{source = "change-completion", statements = ["Every
code-writing handoff must explicitly report the completed code review result
and review recommendations.", "Put a value on separate `Review Result:`,
`Blocking Issues:`, `Required Changes:`, and `Review Recommendations:` lines so
the project Stop hook can validate the handoff after `apply_patch` changes a
non-prose file.", "State the blocking issue count and required changes, and
explicitly state `No changes recomme [...]
critical = true
[[cases]]
diff --git a/.codex/harness/agents/code_handoff_hook.py
b/.codex/harness/agents/code_handoff_hook.py
new file mode 100644
index 00000000000..e60045844f0
--- /dev/null
+++ b/.codex/harness/agents/code_handoff_hook.py
@@ -0,0 +1,133 @@
+#!/usr/bin/env python3
+
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Require an explicit review handoff after Codex changes implementation
files."""
+
+from __future__ import annotations
+
+import hashlib
+import json
+from pathlib import Path
+import re
+import sys
+import tempfile
+from typing import Any
+
+PROSE_SUFFIXES = frozenset({".adoc", ".md", ".rst"})
+PATCH_FILE_PATTERN = re.compile(r"^\*\*\* (?:Add|Delete|Update) File: (.+)$",
re.MULTILINE)
+FIELD_PATTERN =
r"^\s*(?:[-*]\s+)?(?:#{{1,6}}\s+)?(?:\*\*)?{label}:\s*(.*?)(?:\*\*)?\s*$"
+REQUIRED_FIELDS = ("Review Result", "Blocking Issues", "Required Changes",
"Review Recommendations")
+NO_CHANGES_RECOMMENDED = "No changes recommended"
+
+
+def process_event(event: dict[str, Any], state_directory: Path) -> dict[str,
str]:
+ """Process one Codex hook event."""
+ event_name = event.get("hook_event_name")
+ if "PostToolUse" == event_name:
+ _record_implementation_change(event, state_directory)
+ return {}
+ if "Stop" != event_name:
+ return {}
+ marker_path = _marker_path(event, state_directory)
+ if not marker_path.exists():
+ return {}
+ failures = validate_handoff(event.get("last_assistant_message"))
+ if failures:
+ return {"decision": "block", "reason": _continuation_reason(failures)}
+ marker_path.unlink()
+ return {}
+
+
+def _record_implementation_change(event: dict[str, Any], state_directory:
Path) -> None:
+ if "apply_patch" != event.get("tool_name"):
+ return
+ tool_input = event.get("tool_input")
+ patch = tool_input.get("command") if isinstance(tool_input, dict) else None
+ if not isinstance(patch, str) or not _changes_implementation_file(patch):
+ return
+ marker_path = _marker_path(event, state_directory)
+ marker_path.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
+ marker_path.touch(exist_ok=True)
+
+
+def _changes_implementation_file(patch: str) -> bool:
+ paths = PATCH_FILE_PATTERN.findall(patch)
+ return not paths or any(Path(each).suffix.lower() not in PROSE_SUFFIXES
for each in paths)
+
+
+def _marker_path(event: dict[str, Any], state_directory: Path) -> Path:
+ session_id = event.get("session_id")
+ if not isinstance(session_id, str) or not session_id:
+ raise ValueError("Codex hook input is missing session_id")
+ marker_name = hashlib.sha256(session_id.encode("utf-8")).hexdigest()
+ return state_directory / f"{marker_name}.pending"
+
+
+def validate_handoff(message: Any) -> list[str]:
+ """Return every missing or invalid code handoff field."""
+ if not isinstance(message, str) or not message.strip():
+ return list(REQUIRED_FIELDS)
+ values = {field: _field_value(message, field) for field in REQUIRED_FIELDS}
+ failures = [field for field, value in values.items() if not value]
+ blocking_issues = values["Blocking Issues"]
+ if blocking_issues and not blocking_issues.isdigit():
+ failures.append("Blocking Issues must be a non-negative integer")
+ return failures
+ if "0" != blocking_issues:
+ return failures
+ recommendations = values["Review Recommendations"]
+ if NO_CHANGES_RECOMMENDED.casefold() not in recommendations.casefold():
+ failures.append(f'Review Recommendations must state
"{NO_CHANGES_RECOMMENDED}"')
+ if not _field_value(message, "Commit Message"):
+ failures.append("Commit Message")
+ return failures
+
+
+def _field_value(message: str, label: str) -> str:
+ pattern = re.compile(FIELD_PATTERN.format(label=re.escape(label)),
re.IGNORECASE | re.MULTILINE)
+ match = pattern.search(message)
+ return match.group(1).strip() if match else ""
+
+
+def _continuation_reason(failures: list[str]) -> str:
+ missing = ", ".join(failures)
+ return (
+ "The code-writing handoff is incomplete. Correct these fields before
stopping: "
+ f"{missing}. Use explicit Review Result, Blocking Issues, Required
Changes, and Review Recommendations lines. "
+ f'When Blocking Issues is 0, state "{NO_CHANGES_RECOMMENDED}" under
Review Recommendations and include Commit Message.'
+ )
+
+
+def main() -> int:
+ """Read a Codex hook event and write its JSON result."""
+ try:
+ event = json.load(sys.stdin)
+ if not isinstance(event, dict):
+ raise ValueError("Codex hook input must be a JSON object")
+ state_directory = Path(tempfile.gettempdir()) /
"shardingsphere-codex-code-handoff"
+ result = process_event(event, state_directory)
+ except (OSError, TypeError, ValueError, json.JSONDecodeError):
+ result = {
+ "decision": "block",
+ "reason": "The code handoff validator failed closed. Inspect the
project hook before stopping.",
+ }
+ json.dump(result, sys.stdout)
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/.codex/harness/agents/policy-sources.toml
b/.codex/harness/agents/policy-sources.toml
index d6c8d960189..0cd82426664 100644
--- a/.codex/harness/agents/policy-sources.toml
+++ b/.codex/harness/agents/policy-sources.toml
@@ -117,6 +117,27 @@ kind = "harness"
semantic = false
references = []
+[[sources]]
+id = "code-handoff-hook-config"
+path = ".codex/config.toml"
+kind = "harness"
+semantic = false
+references = [".codex/harness/agents/code_handoff_hook.py"]
+
+[[sources]]
+id = "code-handoff-hook"
+path = ".codex/harness/agents/code_handoff_hook.py"
+kind = "harness"
+semantic = false
+references = []
+
+[[sources]]
+id = "code-handoff-hook-tests"
+path = ".codex/harness/agents/test_code_handoff_hook.py"
+kind = "harness"
+semantic = false
+references = [".codex/harness/agents/code_handoff_hook.py"]
+
[[sources]]
id = "policy-source-manifest"
path = ".codex/harness/agents/policy-sources.toml"
diff --git a/.codex/harness/agents/test_code_handoff_hook.py
b/.codex/harness/agents/test_code_handoff_hook.py
new file mode 100644
index 00000000000..b6dbaf4bea3
--- /dev/null
+++ b/.codex/harness/agents/test_code_handoff_hook.py
@@ -0,0 +1,113 @@
+#!/usr/bin/env python3
+
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Test the Codex code handoff hook."""
+
+from __future__ import annotations
+
+import importlib.util
+from pathlib import Path
+import tempfile
+import tomllib
+import unittest
+
+HOOK_SPEC = importlib.util.spec_from_file_location("code_handoff_hook",
Path(__file__).with_name("code_handoff_hook.py"))
+if HOOK_SPEC is None or HOOK_SPEC.loader is None:
+ raise RuntimeError("Unable to load code handoff hook")
+code_handoff_hook = importlib.util.module_from_spec(HOOK_SPEC)
+HOOK_SPEC.loader.exec_module(code_handoff_hook)
+
+
+class CodeHandoffHookTest(unittest.TestCase):
+ """Test the Codex code handoff hook."""
+
+ def test_implementation_patch_requires_complete_handoff(self) -> None:
+ with tempfile.TemporaryDirectory() as directory:
+ state_directory = Path(directory)
+ code_handoff_hook.process_event(self._post_tool_event("*** Update
File: src/main/Foo.java"), state_directory)
+ actual =
code_handoff_hook.process_event(self._stop_event("Implementation complete."),
state_directory)
+ self.assertEqual("block", actual["decision"])
+ self.assertIn("Review Recommendations", actual["reason"])
+ self.assertEqual({},
code_handoff_hook.process_event(self._stop_event(
+ "Review Result: Mergeable\nBlocking Issues: 0\nRequired
Changes: None.\n"
+ "Review Recommendations: No changes recommended.\nCommit
Message: Fix metadata"
+ ), state_directory))
+ self.assertEqual([], list(state_directory.iterdir()))
+
+ def test_prose_patch_does_not_require_code_handoff(self) -> None:
+ with tempfile.TemporaryDirectory() as directory:
+ state_directory = Path(directory)
+ code_handoff_hook.process_event(self._post_tool_event("*** Update
File: docs/foo.md"), state_directory)
+ self.assertEqual({},
code_handoff_hook.process_event(self._stop_event("Documentation updated."),
state_directory))
+
+ def test_zero_blocking_issues_requires_no_changes_recommendation(self) ->
None:
+ actual = code_handoff_hook.validate_handoff(
+ "Review Result: Mergeable\nBlocking Issues: 0\nRequired Changes:
None.\nReview Recommendations: None.\nCommit Message: Fix metadata"
+ )
+ self.assertEqual(['Review Recommendations must state "No changes
recommended"'], actual)
+
+ def test_zero_blocking_issues_requires_commit_message(self) -> None:
+ actual = code_handoff_hook.validate_handoff(
+ "Review Result: Mergeable\nBlocking Issues: 0\nRequired Changes:
None.\nReview Recommendations: No changes recommended."
+ )
+ self.assertEqual(["Commit Message"], actual)
+
+ def test_blocking_handoff_does_not_require_commit_message(self) -> None:
+ actual = code_handoff_hook.validate_handoff(
+ "Review Result: Review Incomplete\nBlocking Issues: 1\nRequired
Changes: Run the unavailable check.\nReview Recommendations: Complete
verification."
+ )
+ self.assertEqual([], actual)
+
+ def test_complete_handoff_clears_pending_marker(self) -> None:
+ with tempfile.TemporaryDirectory() as directory:
+ state_directory = Path(directory)
+ code_handoff_hook.process_event(self._post_tool_event("*** Add
File: scripts/foo.py"), state_directory)
+ actual = code_handoff_hook.process_event(self._stop_event(
+ "**Review Result: Mergeable**\n- Blocking Issues: 0\n-
Required Changes: None.\n"
+ "- Review Recommendations: No changes recommended.\n- Commit
Message: Fix metadata"
+ ), state_directory)
+ self.assertEqual({}, actual)
+ self.assertEqual([], list(state_directory.iterdir()))
+
+ def test_project_config_registers_code_handoff_hooks(self) -> None:
+ config_path = Path(__file__).parents[2] / "config.toml"
+ with config_path.open("rb") as config_file:
+ hooks = tomllib.load(config_file)["hooks"]
+ self.assertEqual("^apply_patch$", hooks["PostToolUse"][0]["matcher"])
+ self.assertIn("code_handoff_hook.py",
hooks["PostToolUse"][0]["hooks"][0]["command"])
+ self.assertIn("code_handoff_hook.py",
hooks["Stop"][0]["hooks"][0]["command"])
+
+ @staticmethod
+ def _post_tool_event(patch: str) -> dict[str, object]:
+ return {
+ "hook_event_name": "PostToolUse",
+ "session_id": "foo_session",
+ "tool_name": "apply_patch",
+ "tool_input": {"command": f"*** Begin Patch\n{patch}\n*** End
Patch"},
+ }
+
+ @staticmethod
+ def _stop_event(message: str) -> dict[str, object]:
+ return {
+ "hook_event_name": "Stop",
+ "session_id": "foo_session",
+ "last_assistant_message": message,
+ }
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git
a/features/encrypt/core/src/main/java/org/apache/shardingsphere/encrypt/rewrite/token/generator/projection/EncryptProjectionTokenGenerator.java
b/features/encrypt/core/src/main/java/org/apache/shardingsphere/encrypt/rewrite/token/generator/projection/EncryptProjectionTokenGenerator.java
index 70f3583ec13..eebe6a084db 100644
---
a/features/encrypt/core/src/main/java/org/apache/shardingsphere/encrypt/rewrite/token/generator/projection/EncryptProjectionTokenGenerator.java
+++
b/features/encrypt/core/src/main/java/org/apache/shardingsphere/encrypt/rewrite/token/generator/projection/EncryptProjectionTokenGenerator.java
@@ -273,7 +273,7 @@ public final class EncryptProjectionTokenGenerator {
"Projections not in simple select, table subquery, join
subquery, predicate subquery and insert select subquery are not supported in
encrypt feature.");
}
- private ColumnProjection generateProjection(final EncryptColumn
encryptColumn, final ColumnProjection columnProjection) {
+ private Projection generateProjection(final EncryptColumn encryptColumn,
final ColumnProjection columnProjection) {
String encryptColumnName = getEncryptColumnName(columnProjection,
encryptColumn);
QuoteCharacter quoteCharacter = getQuoteCharacter(columnProjection);
IdentifierValue cipherColumnName = new
IdentifierValue(encryptColumnName, quoteCharacter);
diff --git
a/infra/binder/dialect/oracle/src/test/java/org/apache/shardingsphere/infra/binder/oracle/OracleProjectionIdentifierExtractorTest.java
b/infra/binder/dialect/oracle/src/test/java/org/apache/shardingsphere/infra/binder/oracle/OracleProjectionIdentifierExtractorTest.java
index 5803efc54a0..b4885ffbff4 100644
---
a/infra/binder/dialect/oracle/src/test/java/org/apache/shardingsphere/infra/binder/oracle/OracleProjectionIdentifierExtractorTest.java
+++
b/infra/binder/dialect/oracle/src/test/java/org/apache/shardingsphere/infra/binder/oracle/OracleProjectionIdentifierExtractorTest.java
@@ -66,6 +66,7 @@ class OracleProjectionIdentifierExtractorTest {
@Test
void assertGetColumnNameFromNullLiteralExpression() {
+ assertThat(extractor.getColumnNameFromExpression(new
ExpressionProjectionSegment(0, 3, null, new LiteralExpressionSegment(0, 3,
null))), is("NULL"));
assertThat(extractor.getColumnNameFromExpression(new
ExpressionProjectionSegment(0, 3, "NULL", new LiteralExpressionSegment(0, 3,
null))), is("NULL"));
}