diff --git a/.github/aicodingflow-tests/test_review_workflow_dispatch.py b/.github/aicodingflow-tests/test_review_workflow_dispatch.py index 354156d..ecce1dd 100644 --- a/.github/aicodingflow-tests/test_review_workflow_dispatch.py +++ b/.github/aicodingflow-tests/test_review_workflow_dispatch.py @@ -15,6 +15,10 @@ def steps(workflow_data: dict, job: str) -> list[dict]: return workflow_data["jobs"][job]["steps"] +def compact(text: str) -> str: + return " ".join(text.split()) + + class ReviewWorkflowDispatchTest(unittest.TestCase): def test_workflows_use_node24_action_runtime(self) -> None: workflow_jobs = { @@ -194,10 +198,21 @@ def test_create_spec_workflow_does_not_dispatch_review_after_pr_creation(self) - self.assertNotIn("actions", data["permissions"]) create_steps = steps(data, "create-spec") + handoff = next(step for step in create_steps if step.get("name") == "Prepare workflow handoff directory") + prepare = next(step for step in create_steps if step.get("name") == "Prepare issue context") + validate = next(step for step in create_steps if step.get("name") == "Validate spec output") create_pr = next(step for step in create_steps if step.get("name") == "Create or update spec pull request") step_names = [step.get("name") for step in create_steps] + self.assertEqual(handoff["id"], "handoff") + self.assertIn("$GITHUB_WORKSPACE/.codex-runtime/handoff", handoff["run"]) + self.assertIn('${{ steps.handoff.outputs.dir }}/issue_context.json', prepare["run"]) + self.assertIn('${{ steps.handoff.outputs.dir }}/issue_comments.txt', prepare["run"]) + self.assertIn('${{ steps.handoff.outputs.dir }}/issue_context.json', validate["run"]) + self.assertIn('${{ steps.handoff.outputs.dir }}/pr-metadata.json', validate["run"]) self.assertIn(".github/scripts/finalize_spec_pr.py", create_pr["run"]) + self.assertIn('${{ steps.handoff.outputs.dir }}/issue_context.json', create_pr["run"]) + self.assertIn('${{ steps.handoff.outputs.dir }}/pr-metadata.json', create_pr["run"]) self.assertNotIn("--github-output", create_pr["run"]) self.assertNotIn("id", create_pr) self.assertNotIn("Dispatch AI PR review", step_names) @@ -207,12 +222,29 @@ def test_create_implementation_workflow_does_not_dispatch_review_after_pr_creati self.assertNotIn("actions", data["permissions"]) create_steps = steps(data, "create-implementation") + handoff = next(step for step in create_steps if step.get("name") == "Prepare workflow handoff directory") + prepare = next(step for step in create_steps if step.get("name") == "Prepare implementation context") + worktree = next(step for step in create_steps if step.get("name") == "Check implementation worktree changes") + validate = next(step for step in create_steps if step.get("name") == "Validate implementation output") workflow_update = next(step for step in create_steps if step.get("name") == "Check whether workflow update token is required") app_token = next(step for step in create_steps if step.get("name") == "Create GitHub App token for workflow updates") commit = next(step for step in create_steps if step.get("name") == "Commit and push implementation branch") create_pr = next(step for step in create_steps if step.get("name") == "Create or update implementation pull request") step_names = [step.get("name") for step in create_steps] + self.assertEqual(handoff["id"], "handoff") + self.assertIn("$GITHUB_WORKSPACE/.codex-runtime/handoff", handoff["run"]) + self.assertIn('${{ steps.handoff.outputs.dir }}/issue_context.json', prepare["run"]) + self.assertIn('${{ steps.handoff.outputs.dir }}/issue_comments.txt', prepare["run"]) + self.assertIn('${{ steps.handoff.outputs.dir }}/spec_context.md', prepare["run"]) + self.assertNotIn("':!issue_context.json'", worktree["run"]) + self.assertNotIn("':!pr-metadata.json'", worktree["run"]) + self.assertIn("':!.codex-runtime'", worktree["run"]) + self.assertIn("':!.codex-runtime/**'", worktree["run"]) + self.assertIn('${{ steps.handoff.outputs.dir }}/issue_context.json', validate["run"]) + self.assertIn('${{ steps.handoff.outputs.dir }}/pr-metadata.json', validate["run"]) + self.assertIn('${{ steps.handoff.outputs.dir }}/validation-output.txt', validate["run"]) + self.assertIn('${{ steps.handoff.outputs.dir }}/validation-error.txt', validate["run"]) self.assertLess(step_names.index("Check whether workflow update token is required"), step_names.index("Create GitHub App token for workflow updates")) self.assertLess(step_names.index("Create GitHub App token for workflow updates"), step_names.index("Commit and push implementation branch")) self.assertEqual(workflow_update["id"], "workflow_update") @@ -228,8 +260,12 @@ def test_create_implementation_workflow_does_not_dispatch_review_after_pr_creati self.assertEqual(app_token["with"]["permission-workflows"], "write") self.assertNotIn("steps.workflow_update.outputs.required", commit["if"]) self.assertEqual(commit["env"]["WORKFLOW_UPDATE_TOKEN"], "${{ steps.app-token.outputs.token }}") + self.assertIn('${{ steps.handoff.outputs.dir }}/issue_context.json', commit["run"]) + self.assertIn('${{ steps.handoff.outputs.dir }}/pr-metadata.json', commit["run"]) self.assertEqual(create_pr["id"], "pr") self.assertIn("--github-output \"$GITHUB_OUTPUT\"", create_pr["run"]) + self.assertIn('${{ steps.handoff.outputs.dir }}/issue_context.json', create_pr["run"]) + self.assertIn('${{ steps.handoff.outputs.dir }}/pr-metadata.json', create_pr["run"]) self.assertNotIn("Dispatch AI PR review", step_names) def test_update_dedupe_pr_body_includes_captured_evidence_summary(self) -> None: @@ -286,12 +322,23 @@ def test_respond_to_pr_comment_workflow_has_secure_triggers_and_gates(self) -> N self.assertEqual(respond_job["permissions"]["issues"], "write") preflight_steps = steps(data, "preflight") + preflight_handoff = next(step for step in preflight_steps if step.get("name") == "Prepare workflow handoff directory") prepare = next(step for step in preflight_steps if step.get("name") == "Prepare PR comment context") + self.assertEqual(preflight_handoff["id"], "handoff") + self.assertIn("$RUNNER_TEMP/aicodingflow-pr-comment-preflight", preflight_handoff["run"]) self.assertIn(".github/scripts/prepare_pr_comment_context.py", prepare["run"]) + self.assertIn('${{ steps.handoff.outputs.dir }}/pr_comment_context.json', prepare["run"]) + self.assertIn('${{ steps.handoff.outputs.dir }}/pr_event.json', prepare["run"]) + self.assertIn('${{ steps.handoff.outputs.dir }}/review_comment_ids.json', prepare["run"]) self.assertIn("--github-output \"$GITHUB_OUTPUT\"", prepare["run"]) respond_steps = steps(data, "respond") + handoff = next(step for step in respond_steps if step.get("name") == "Prepare workflow handoff directory") checkout = next(step for step in respond_steps if step.get("name") == "Checkout PR head") + self.assertEqual(handoff["id"], "handoff") + self.assertIn("$RUNNER_TEMP/aicodingflow-pr-comment-response", handoff["run"]) + self.assertIn("$GITHUB_WORKSPACE/pr-worktree/.codex-runtime/handoff", handoff["run"]) + self.assertIn("temp_dir=", handoff["run"]) self.assertIn("steps.context.outputs.should_run == 'true'", checkout["if"]) self.assertIn("steps.context.outputs.branch_strategy != 'blocked'", checkout["if"]) self.assertEqual(checkout["with"]["persist-credentials"], False) @@ -315,7 +362,7 @@ def test_respond_to_pr_comment_workflow_has_secure_triggers_and_gates(self) -> N self.assertIn("--pr-number \"${{ steps.context.outputs.pr_number }}\"", diff_step["run"]) self.assertIn("--head-sha \"${{ steps.context.outputs.head_sha }}\"", diff_step["run"]) self.assertIn("--base-sha \"${{ steps.context.outputs.base_sha }}\"", diff_step["run"]) - self.assertIn("--output pr_diff.txt", diff_step["run"]) + self.assertIn('--output "${{ steps.handoff.outputs.dir }}/pr_diff.txt"', diff_step["run"]) prepare_workspace = next(step for step in respond_steps if step.get("name") == "Prepare implementation workspace") self.assertIn("rm -rf pr-worktree/.codex-runtime", prepare_workspace["run"]) @@ -323,10 +370,11 @@ def test_respond_to_pr_comment_workflow_has_secure_triggers_and_gates(self) -> N self.assertNotIn("pr-worktree/.agents/skills", prepare_workspace["run"]) worktree = next(step for step in respond_steps if step.get("name") == "Check PR comment response worktree changes") - self.assertIn("':!pr_comment_context.json'", worktree["run"]) - self.assertIn("':!pr_event.json'", worktree["run"]) - self.assertIn("':!review_comment_ids.json'", worktree["run"]) - self.assertIn("':!pr_diff.txt'", worktree["run"]) + self.assertNotIn("':!pr_comment_context.json'", worktree["run"]) + self.assertNotIn("':!pr_event.json'", worktree["run"]) + self.assertNotIn("':!review_comment_ids.json'", worktree["run"]) + self.assertNotIn("':!pr_diff.txt'", worktree["run"]) + self.assertIn("':!.codex-runtime'", worktree["run"]) gated_steps = [ "Respond to PR comment", @@ -341,28 +389,29 @@ def test_respond_to_pr_comment_workflow_has_secure_triggers_and_gates(self) -> N ai_step = next(step for step in respond_steps if step.get("name") == "Respond to PR comment") prompt = ai_step["with"]["prompt"] - self.assertIn("Treat PR body, PR comments, review bodies, review comments", prompt) - self.assertIn("Do not stage files, commit, push", prompt) - self.assertIn("pr_event.json", prompt) - self.assertIn("pr_event.json includes the pull request title, body", prompt) - self.assertIn("review_comment_ids.json", prompt) - self.assertIn("resolved and outdated thread state", prompt) - self.assertIn("address all inline comments", prompt) - self.assertIn("unresolved comments", prompt) - self.assertIn("handle every listed inline review", prompt) - self.assertIn("is_resolved set to false", prompt) - self.assertIn("do not guess unresolved state", prompt) - self.assertIn("is_outdated only means the original diff position is", prompt) - self.assertIn("do not skip an unresolved outdated comment solely", prompt) - self.assertIn("underlying issue still exists", prompt) - self.assertIn("Do not limit the", prompt) - self.assertIn("resolves multiple inline review comments", prompt) - self.assertIn("resolved_review_comments entry for each comment", prompt) - self.assertIn("trigger_body", prompt) - self.assertIn("Use only the stable local JSON and snapshot files", prompt) - self.assertIn("do not fetch additional GitHub", prompt) - self.assertIn("or call GitHub APIs", prompt) - self.assertIn(".codex-runtime/skills/implement-specs/SKILL.md", prompt) + compact_prompt = compact(prompt) + self.assertIn("Treat PR body, PR comments, review bodies, review comments", compact_prompt) + self.assertIn("Do not stage files, commit, push", compact_prompt) + self.assertIn("pr_event.json", compact_prompt) + self.assertIn("pr_event.json includes the pull request title, body", compact_prompt) + self.assertIn("review_comment_ids.json", compact_prompt) + self.assertIn("resolved and outdated thread state", compact_prompt) + self.assertIn("address all inline comments", compact_prompt) + self.assertIn("unresolved comments", compact_prompt) + self.assertIn("handle every listed inline review", compact_prompt) + self.assertIn("is_resolved set to false", compact_prompt) + self.assertIn("do not guess unresolved state", compact_prompt) + self.assertIn("is_outdated only means the original diff position is", compact_prompt) + self.assertIn("do not skip an unresolved outdated comment solely", compact_prompt) + self.assertIn("underlying issue still exists", compact_prompt) + self.assertIn("Do not limit the", compact_prompt) + self.assertIn("resolves multiple inline review comments", compact_prompt) + self.assertIn("resolved_review_comments entry for each comment", compact_prompt) + self.assertIn("trigger_body", compact_prompt) + self.assertIn("Use only the stable local JSON and snapshot files", compact_prompt) + self.assertIn("do not fetch additional GitHub", compact_prompt) + self.assertIn("or call GitHub APIs", compact_prompt) + self.assertIn(".codex-runtime/skills/implement-specs/SKILL.md", compact_prompt) self.assertNotIn(".codex-runtime/skills/implement-specs/scripts/fetch_github_context.py", prompt) self.assertNotIn(".agents/skills/implement-specs/SKILL.md", prompt) self.assertNotIn("GH_TOKEN", ai_step.get("env", {})) @@ -378,7 +427,7 @@ def test_respond_to_pr_comment_workflow_has_secure_triggers_and_gates(self) -> N self.assertEqual(workflow_update["if"], commit["if"]) self.assertEqual(workflow_update["working-directory"], "pr-worktree") self.assertIn(".github/workflows/", workflow_update["run"]) - self.assertIn("pr-metadata.json", workflow_update["run"]) + self.assertIn('${{ steps.handoff.outputs.dir }}/pr-metadata.json', workflow_update["run"]) self.assertEqual(app_token["id"], "app-token") self.assertEqual(app_token["uses"], "actions/create-github-app-token@v3") self.assertEqual(app_token["if"], commit["if"] + " && steps.workflow_update.outputs.required == 'true'") @@ -389,6 +438,8 @@ def test_respond_to_pr_comment_workflow_has_secure_triggers_and_gates(self) -> N self.assertEqual(commit["env"]["GITHUB_TOKEN"], "${{ github.token }}") self.assertNotIn("steps.workflow_update.outputs.required", commit["if"]) self.assertEqual(commit["env"]["WORKFLOW_UPDATE_TOKEN"], "${{ steps.app-token.outputs.token }}") + self.assertIn('${{ steps.handoff.outputs.dir }}/pr_comment_context.json', commit["run"]) + self.assertIn('${{ steps.handoff.outputs.dir }}/pr-metadata.json', commit["run"]) if __name__ == "__main__": diff --git a/.github/aicodingflow-tests/test_validate_spec_output.py b/.github/aicodingflow-tests/test_validate_spec_output.py index 0f5f0c1..b00a62a 100644 --- a/.github/aicodingflow-tests/test_validate_spec_output.py +++ b/.github/aicodingflow-tests/test_validate_spec_output.py @@ -189,6 +189,26 @@ def test_validate_write_surface_ignores_python_cache_files(self) -> None: } ) + def test_validate_write_surface_ignores_codex_runtime_handoff(self) -> None: + result = subprocess.CompletedProcess( + args=[], + returncode=0, + stdout=( + " M specs/example/product.md\n" + "?? .codex-runtime/handoff/issue_context.json\n" + "?? .codex-runtime/handoff/issue_comments.txt\n" + "?? .codex-runtime/handoff/pr-metadata.json\n" + ), + ) + + with mock.patch.object(validate_spec_output.subprocess, "run", return_value=result): + validate_spec_output.validate_write_surface( + { + "specs/example/product.md", + "specs/example/tech.md", + } + ) + if __name__ == "__main__": unittest.main() diff --git a/.github/scripts/validate_spec_output.py b/.github/scripts/validate_spec_output.py index bbe29ea..f5ba128 100644 --- a/.github/scripts/validate_spec_output.py +++ b/.github/scripts/validate_spec_output.py @@ -65,7 +65,11 @@ def changed_paths() -> set[str]: def is_ignored_generated_path(path: str) -> bool: parts = Path(path).parts - return "__pycache__" in parts or path.endswith((".pyc", ".pyo")) + return ( + bool(parts and parts[0] == ".codex-runtime") + or "__pycache__" in parts + or path.endswith((".pyc", ".pyo")) + ) def validate_write_surface(allowed_paths: set[str]) -> None: diff --git a/.github/workflows/create-implementation-from-issue.yml b/.github/workflows/create-implementation-from-issue.yml index bca0297..37929f4 100644 --- a/.github/workflows/create-implementation-from-issue.yml +++ b/.github/workflows/create-implementation-from-issue.yml @@ -49,6 +49,14 @@ jobs: fetch-depth: 0 ref: ${{ github.event.repository.default_branch }} + - name: Prepare workflow handoff directory + id: handoff + run: | + dir="$GITHUB_WORKSPACE/.codex-runtime/handoff" + rm -rf "$dir" + mkdir -p "$dir" + echo "dir=$dir" >> "$GITHUB_OUTPUT" + - name: Prepare implementation context id: issue env: @@ -62,9 +70,9 @@ jobs: --event-name "${{ github.event_name }}" \ --event-path "$GITHUB_EVENT_PATH" \ --agent-login "$AGENT_LOGIN" \ - --output issue_context.json \ - --comments-output issue_comments.txt \ - --spec-context-output spec_context.md \ + --output "${{ steps.handoff.outputs.dir }}/issue_context.json" \ + --comments-output "${{ steps.handoff.outputs.dir }}/issue_comments.txt" \ + --spec-context-output "${{ steps.handoff.outputs.dir }}/spec_context.md" \ --github-output "$GITHUB_OUTPUT" - name: Stop when issue is not ready @@ -81,7 +89,7 @@ jobs: run: | python3 .github/scripts/update_implementation_progress.py \ --repo "${{ github.repository }}" \ - --context issue_context.json \ + --context "${{ steps.handoff.outputs.dir }}/issue_context.json" \ --status "blocked" \ --message "${{ steps.issue.outputs.noop_reason }}" @@ -118,7 +126,7 @@ jobs: python3 .github/scripts/read_branch_sha.py \ --repo "${{ github.repository }}" \ --branch "$TARGET_BRANCH" \ - --snapshot-output branch-start-shas.json \ + --snapshot-output "${{ steps.handoff.outputs.dir }}/branch-start-shas.json" \ --github-output "$GITHUB_OUTPUT" - name: Checkout implementation target branch @@ -141,8 +149,8 @@ jobs: - name: Prepare implementation output directory if: steps.issue.outputs.should_run == 'true' && steps.issue.outputs.should_noop != 'true' run: | - rm -rf implementation-output - mkdir -p implementation-output + rm -rf "${{ steps.handoff.outputs.dir }}/implementation-output" + mkdir -p "${{ steps.handoff.outputs.dir }}/implementation-output" - name: Implement issue if: steps.issue.outputs.should_run == 'true' && steps.issue.outputs.should_noop != 'true' @@ -155,18 +163,18 @@ jobs: This workflow is the implementation phase of the spec-driven workflow, not the spec-writing phase. Do not create a new product - or tech spec plan from scratch. Implement against spec_context.md - when it exists, and update specs only when the implementation - materially changes behavior, edge cases, validation expectations, - or technical design. + or tech spec plan from scratch. Implement against + ${{ steps.handoff.outputs.dir }}/spec_context.md when it exists, + and update specs only when the implementation materially changes + behavior, edge cases, validation expectations, or technical design. Inputs: - - issue_context.json - - issue_comments.txt - - spec_context.md when it exists + - ${{ steps.handoff.outputs.dir }}/issue_context.json + - ${{ steps.handoff.outputs.dir }}/issue_comments.txt + - ${{ steps.handoff.outputs.dir }}/spec_context.md when it exists - Read issue_context.json first. Then read and use these skills in - this exact order: + Read ${{ steps.handoff.outputs.dir }}/issue_context.json first. + Then read and use these skills in this exact order: 1. .agents/skills/implement-specs/SKILL.md 2. .agents/skills/spec-driven-implementation/SKILL.md 3. .agents/skills/implement-issue/SKILL.md @@ -176,26 +184,29 @@ jobs: workflow guidance. Implement the issue conservatively and keep specs aligned with the - actual implementation. If spec_context.md exists, use it as the - primary design context. If it does not exist, make the smallest + actual implementation. If + ${{ steps.handoff.outputs.dir }}/spec_context.md exists, use it as + the primary design context. If it does not exist, make the smallest reasonable implementation choice from the issue context and document assumptions. Required outputs: - - implementation_summary.md - - pr-metadata.json + - ${{ steps.handoff.outputs.dir }}/implementation_summary.md + - ${{ steps.handoff.outputs.dir }}/pr-metadata.json - implementation file changes in the working tree if there are changes If a required implementation file is under .agents and .agents is read-only in the Codex sandbox, write the complete replacement file - under implementation-output using the same repository-relative path - instead. For example, write - implementation-output/.agents/skills/example/SKILL.md for target - file .agents/skills/example/SKILL.md. The outer workflow will apply - those files to the working tree after Codex exits. Do not use - implementation-output for files outside .agents. - - pr-metadata.json must be a JSON object with: + under ${{ steps.handoff.outputs.dir }}/implementation-output using + the same repository-relative path instead. For example, write + ${{ steps.handoff.outputs.dir }}/implementation-output/.agents/skills/example/SKILL.md + for target file .agents/skills/example/SKILL.md. The outer workflow + will apply those files to the working tree after Codex exits. Do + not use the implementation output directory for files outside + .agents. + + ${{ steps.handoff.outputs.dir }}/pr-metadata.json must be a JSON + object with: - branch_name: the branch the outer workflow should commit and push - pr_title: conventional commit style title based on actual changes - pr_summary: complete markdown PR body whose first line is exactly @@ -210,34 +221,33 @@ jobs: Do not stage files, commit, push, create branches, or run GitHub CLI commands. Leave implementation changes in the working tree. - The outer workflow validates pr-metadata.json, commits the - implementation files to pr-metadata.json.branch_name, pushes that - branch, creates or updates the PR, and posts progress updates. + The outer workflow validates + ${{ steps.handoff.outputs.dir }}/pr-metadata.json, commits the + implementation files to that metadata file's branch_name, pushes + that branch, creates or updates the PR, and posts progress updates. - Respect issue_context.json.coauthor_directives when relevant, but - do not invent coauthors. + Respect coauthor_directives from + ${{ steps.handoff.outputs.dir }}/issue_context.json when relevant, + but do not invent coauthors. Do not create or update pull requests, post GitHub comments, or edit GitHub issues. The outer workflow handles GitHub API writes - after validating pr-metadata.json. + after validating ${{ steps.handoff.outputs.dir }}/pr-metadata.json. - name: Apply implementation output if: steps.issue.outputs.should_run == 'true' && steps.issue.outputs.should_noop != 'true' - run: python3 .github/scripts/apply_implementation_output.py + run: | + python3 .github/scripts/apply_implementation_output.py \ + --output-dir "${{ steps.handoff.outputs.dir }}/implementation-output" \ + --metadata "${{ steps.handoff.outputs.dir }}/pr-metadata.json" - name: Check implementation worktree changes if: steps.issue.outputs.should_run == 'true' && steps.issue.outputs.should_noop != 'true' id: worktree run: | changed="$(git status --porcelain=v1 --untracked-files=all -- \ - ':!issue_context.json' \ - ':!issue_comments.txt' \ - ':!spec_context.md' \ - ':!branch-start-shas.json' \ - ':!implementation_summary.md' \ - ':!pr-metadata.json' \ - ':!validation-output.txt' \ - ':!validation-error.txt')" + ':!.codex-runtime' \ + ':!.codex-runtime/**')" if [ -n "$changed" ]; then echo "changed=true" >> "$GITHUB_OUTPUT" else @@ -251,7 +261,7 @@ jobs: run: | python3 .github/scripts/update_implementation_progress.py \ --repo "${{ github.repository }}" \ - --context issue_context.json \ + --context "${{ steps.handoff.outputs.dir }}/issue_context.json" \ --status "no changes" \ --message "I analyzed this issue but did not produce an implementation diff." @@ -261,10 +271,10 @@ jobs: continue-on-error: true run: | python3 .github/scripts/validate_implementation_output.py \ - --context issue_context.json \ - --metadata pr-metadata.json \ - > validation-output.txt \ - 2> validation-error.txt + --context "${{ steps.handoff.outputs.dir }}/issue_context.json" \ + --metadata "${{ steps.handoff.outputs.dir }}/pr-metadata.json" \ + > "${{ steps.handoff.outputs.dir }}/validation-output.txt" \ + 2> "${{ steps.handoff.outputs.dir }}/validation-error.txt" - name: Update progress comment for invalid implementation output if: steps.validate.outcome == 'failure' @@ -273,9 +283,9 @@ jobs: run: | python3 .github/scripts/update_implementation_progress.py \ --repo "${{ github.repository }}" \ - --context issue_context.json \ + --context "${{ steps.handoff.outputs.dir }}/issue_context.json" \ --status "failed" \ - --message-file validation-error.txt + --message-file "${{ steps.handoff.outputs.dir }}/validation-error.txt" - name: Check whether workflow update token is required if: steps.issue.outputs.should_run == 'true' && steps.issue.outputs.should_noop != 'true' && steps.worktree.outputs.changed == 'true' && steps.validate.outcome == 'success' @@ -284,7 +294,7 @@ jobs: python3 - <<'PY' >> "$GITHUB_OUTPUT" import json - with open("pr-metadata.json", encoding="utf-8") as handle: + with open("${{ steps.handoff.outputs.dir }}/pr-metadata.json", encoding="utf-8") as handle: metadata = json.load(handle) intended_files = metadata.get("intended_files") or [] required = any(str(path).startswith(".github/workflows/") for path in intended_files) @@ -308,8 +318,8 @@ jobs: WORKFLOW_UPDATE_TOKEN: ${{ steps.app-token.outputs.token }} run: | python3 .github/scripts/commit_implementation_branch.py \ - --context issue_context.json \ - --metadata pr-metadata.json \ + --context "${{ steps.handoff.outputs.dir }}/issue_context.json" \ + --metadata "${{ steps.handoff.outputs.dir }}/pr-metadata.json" \ --github-output "$GITHUB_OUTPUT" - name: Stop when implementation produced no committable diff @@ -319,7 +329,7 @@ jobs: run: | python3 .github/scripts/update_implementation_progress.py \ --repo "${{ github.repository }}" \ - --context issue_context.json \ + --context "${{ steps.handoff.outputs.dir }}/issue_context.json" \ --status "no changes" \ --message "I analyzed this issue but did not produce a committable implementation diff." @@ -333,8 +343,8 @@ jobs: python3 .github/scripts/read_branch_sha.py \ --repo "${{ github.repository }}" \ --branch "$TARGET_BRANCH" \ - --metadata pr-metadata.json \ - --snapshot branch-start-shas.json \ + --metadata "${{ steps.handoff.outputs.dir }}/pr-metadata.json" \ + --snapshot "${{ steps.handoff.outputs.dir }}/branch-start-shas.json" \ --github-output "$GITHUB_OUTPUT" - name: Create or update implementation pull request @@ -345,8 +355,8 @@ jobs: run: | python3 .github/scripts/finalize_implementation_pr.py \ --repo "${{ github.repository }}" \ - --context issue_context.json \ - --metadata pr-metadata.json \ + --context "${{ steps.handoff.outputs.dir }}/issue_context.json" \ + --metadata "${{ steps.handoff.outputs.dir }}/pr-metadata.json" \ --github-output "$GITHUB_OUTPUT" - name: Update progress comment @@ -356,7 +366,7 @@ jobs: run: | python3 .github/scripts/update_implementation_progress.py \ --repo "${{ github.repository }}" \ - --context issue_context.json \ + --context "${{ steps.handoff.outputs.dir }}/issue_context.json" \ --status "ready for review" \ --message "Review the implementation changes in the PR. Complete any manual verification needed before merging." \ --pr-url "${{ steps.pr.outputs.pr_url }}" @@ -370,12 +380,12 @@ jobs: with: name: create-implementation-from-issue-${{ github.run_id }} path: | - issue_context.json - issue_comments.txt - spec_context.md - branch-start-shas.json - implementation_summary.md - pr-metadata.json - implementation-output - validation-output.txt - validation-error.txt + ${{ steps.handoff.outputs.dir }}/issue_context.json + ${{ steps.handoff.outputs.dir }}/issue_comments.txt + ${{ steps.handoff.outputs.dir }}/spec_context.md + ${{ steps.handoff.outputs.dir }}/branch-start-shas.json + ${{ steps.handoff.outputs.dir }}/implementation_summary.md + ${{ steps.handoff.outputs.dir }}/pr-metadata.json + ${{ steps.handoff.outputs.dir }}/implementation-output + ${{ steps.handoff.outputs.dir }}/validation-output.txt + ${{ steps.handoff.outputs.dir }}/validation-error.txt diff --git a/.github/workflows/create-spec-from-issue.yml b/.github/workflows/create-spec-from-issue.yml index 6d09a95..560e914 100644 --- a/.github/workflows/create-spec-from-issue.yml +++ b/.github/workflows/create-spec-from-issue.yml @@ -51,6 +51,14 @@ jobs: fetch-depth: 0 ref: ${{ github.event.repository.default_branch }} + - name: Prepare workflow handoff directory + id: handoff + run: | + dir="$GITHUB_WORKSPACE/.codex-runtime/handoff" + rm -rf "$dir" + mkdir -p "$dir" + echo "dir=$dir" >> "$GITHUB_OUTPUT" + - name: Prepare issue context id: issue env: @@ -64,8 +72,8 @@ jobs: --event-name "${{ github.event_name }}" \ --event-path "$GITHUB_EVENT_PATH" \ --agent-login "$AGENT_LOGIN" \ - --output issue_context.json \ - --comments-output issue_comments.txt \ + --output "${{ steps.handoff.outputs.dir }}/issue_context.json" \ + --comments-output "${{ steps.handoff.outputs.dir }}/issue_comments.txt" \ --github-output "$GITHUB_OUTPUT" - name: Stop when issue is not ready @@ -106,11 +114,11 @@ jobs: Create GitHub issue specs from the stable local context files. Inputs: - - issue_context.json - - issue_comments.txt + - ${{ steps.handoff.outputs.dir }}/issue_context.json + - ${{ steps.handoff.outputs.dir }}/issue_comments.txt - Read issue_context.json first. Then read and use these skills in - this exact order: + Read ${{ steps.handoff.outputs.dir }}/issue_context.json first. + Then read and use these skills in this exact order: 1. .agents/skills/spec-driven-implementation/SKILL.md - Overall workflow: decide and organize spec-first work. 2. .agents/skills/write-product-spec/SKILL.md @@ -125,20 +133,23 @@ jobs: Required outputs: - ${{ steps.issue.outputs.product_spec }} - ${{ steps.issue.outputs.tech_spec }} - - pr-metadata.json + - ${{ steps.handoff.outputs.dir }}/pr-metadata.json This workflow is only for writing specs. Produce product.md, - tech.md, and pr-metadata.json only. Do not implement the feature, - modify production code, or begin the implement-specs phase. + tech.md, and ${{ steps.handoff.outputs.dir }}/pr-metadata.json + only. Do not implement the feature, modify production code, or + begin the implement-specs phase. - Write pr-metadata.json as a JSON object with: - - branch_name: issue_context.json.target_branch + Write ${{ steps.handoff.outputs.dir }}/pr-metadata.json as a JSON + object with: + - branch_name: target_branch from ${{ steps.handoff.outputs.dir }}/issue_context.json - pr_title: concise conventional-commit style PR title, such as "docs(spec): create issue ${{ steps.issue.outputs.issue_number }} specs" - pr_summary: complete markdown PR body. It must include the exact non-closing issue reference "Refs #${{ steps.issue.outputs.issue_number }}". - Respect issue_context.json.coauthor_directives in generated + Respect coauthor_directives from + ${{ steps.handoff.outputs.dir }}/issue_context.json in generated metadata or specs when relevant, but do not invent coauthors. Do not modify .agents, .github, tests, README, or production code. @@ -146,7 +157,10 @@ jobs: - name: Validate spec output if: steps.issue.outputs.should_run == 'true' - run: python3 .github/scripts/validate_spec_output.py --context issue_context.json --metadata pr-metadata.json + run: | + python3 .github/scripts/validate_spec_output.py \ + --context "${{ steps.handoff.outputs.dir }}/issue_context.json" \ + --metadata "${{ steps.handoff.outputs.dir }}/pr-metadata.json" - name: Check for spec changes if: steps.issue.outputs.should_run == 'true' @@ -165,14 +179,14 @@ jobs: run: | python3 .github/scripts/finalize_spec_pr.py \ --repo "${{ github.repository }}" \ - --context issue_context.json \ - --metadata pr-metadata.json + --context "${{ steps.handoff.outputs.dir }}/issue_context.json" \ + --metadata "${{ steps.handoff.outputs.dir }}/pr-metadata.json" - uses: actions/upload-artifact@v7 if: always() with: name: create-spec-from-issue-${{ github.run_id }} path: | - issue_context.json - issue_comments.txt - pr-metadata.json + ${{ steps.handoff.outputs.dir }}/issue_context.json + ${{ steps.handoff.outputs.dir }}/issue_comments.txt + ${{ steps.handoff.outputs.dir }}/pr-metadata.json diff --git a/.github/workflows/respond-to-pr-comment.yml b/.github/workflows/respond-to-pr-comment.yml index 844ca0d..5e0b542 100644 --- a/.github/workflows/respond-to-pr-comment.yml +++ b/.github/workflows/respond-to-pr-comment.yml @@ -60,6 +60,14 @@ jobs: with: fetch-depth: 1 + - name: Prepare workflow handoff directory + id: handoff + run: | + dir="$RUNNER_TEMP/aicodingflow-pr-comment-preflight" + rm -rf "$dir" + mkdir -p "$dir" + echo "dir=$dir" >> "$GITHUB_OUTPUT" + - name: Prepare PR comment context id: context env: @@ -70,9 +78,9 @@ jobs: --event-name "${{ github.event_name }}" \ --event-path "$GITHUB_EVENT_PATH" \ --agent-login "${{ vars.AGENT_LOGIN }}" \ - --output pr_comment_context.json \ - --pr-event-output pr_event.json \ - --review-comment-ids-output review_comment_ids.json \ + --output "${{ steps.handoff.outputs.dir }}/pr_comment_context.json" \ + --pr-event-output "${{ steps.handoff.outputs.dir }}/pr_event.json" \ + --review-comment-ids-output "${{ steps.handoff.outputs.dir }}/review_comment_ids.json" \ --github-output "$GITHUB_OUTPUT" - name: Report skipped PR comment response @@ -84,9 +92,9 @@ jobs: with: name: respond-to-pr-comment-preflight-${{ github.run_id }} path: | - pr_comment_context.json - pr_event.json - review_comment_ids.json + ${{ steps.handoff.outputs.dir }}/pr_comment_context.json + ${{ steps.handoff.outputs.dir }}/pr_event.json + ${{ steps.handoff.outputs.dir }}/review_comment_ids.json respond: needs: preflight @@ -106,6 +114,16 @@ jobs: with: fetch-depth: 1 + - name: Prepare workflow handoff directory + id: handoff + run: | + temp_dir="$RUNNER_TEMP/aicodingflow-pr-comment-response" + dir="$GITHUB_WORKSPACE/pr-worktree/.codex-runtime/handoff" + rm -rf "$temp_dir" + mkdir -p "$temp_dir" + echo "temp_dir=$temp_dir" >> "$GITHUB_OUTPUT" + echo "dir=$dir" >> "$GITHUB_OUTPUT" + - name: Prepare PR comment context id: context env: @@ -116,9 +134,9 @@ jobs: --event-name "${{ github.event_name }}" \ --event-path "$GITHUB_EVENT_PATH" \ --agent-login "${{ vars.AGENT_LOGIN }}" \ - --output pr_comment_context.json \ - --pr-event-output pr_event.json \ - --review-comment-ids-output review_comment_ids.json \ + --output "${{ steps.handoff.outputs.temp_dir }}/pr_comment_context.json" \ + --pr-event-output "${{ steps.handoff.outputs.temp_dir }}/pr_event.json" \ + --review-comment-ids-output "${{ steps.handoff.outputs.temp_dir }}/review_comment_ids.json" \ --github-output "$GITHUB_OUTPUT" - name: Checkout PR head @@ -131,12 +149,15 @@ jobs: ref: ${{ steps.context.outputs.head_sha }} path: pr-worktree - - name: Copy stable context into PR worktree + - name: Prepare implementation workspace if: steps.context.outputs.should_run == 'true' && steps.context.outputs.branch_strategy != 'blocked' run: | - cp pr_comment_context.json pr-worktree/pr_comment_context.json - cp pr_event.json pr-worktree/pr_event.json - cp review_comment_ids.json pr-worktree/review_comment_ids.json + rm -rf pr-worktree/.codex-runtime + mkdir -p "${{ steps.handoff.outputs.dir }}" + cp "${{ steps.handoff.outputs.temp_dir }}/pr_comment_context.json" "${{ steps.handoff.outputs.dir }}/pr_comment_context.json" + cp "${{ steps.handoff.outputs.temp_dir }}/pr_event.json" "${{ steps.handoff.outputs.dir }}/pr_event.json" + cp "${{ steps.handoff.outputs.temp_dir }}/review_comment_ids.json" "${{ steps.handoff.outputs.dir }}/review_comment_ids.json" + cp -R .agents/skills pr-worktree/.codex-runtime/skills - name: Snapshot PR diff if: steps.context.outputs.should_run == 'true' && steps.context.outputs.branch_strategy != 'blocked' @@ -149,25 +170,18 @@ jobs: --pr-number "${{ steps.context.outputs.pr_number }}" \ --head-sha "${{ steps.context.outputs.head_sha }}" \ --base-sha "${{ steps.context.outputs.base_sha }}" \ - --output pr_diff.txt + --output "${{ steps.handoff.outputs.dir }}/pr_diff.txt" - name: Snapshot spec context if: steps.context.outputs.should_run == 'true' && steps.context.outputs.branch_strategy != 'blocked' env: - PR_EVENT_PATH: pr_event.json + PR_EVENT_PATH: ${{ steps.handoff.outputs.dir }}/pr_event.json GH_TOKEN: ${{ github.token }} GITHUB_TOKEN: ${{ github.token }} run: | python3 .github/scripts/write_spec_context.py \ - --changed-files-from-diff pr-worktree/pr_diff.txt \ - --output pr-worktree/spec_context.md - - - name: Prepare implementation workspace - if: steps.context.outputs.should_run == 'true' && steps.context.outputs.branch_strategy != 'blocked' - run: | - rm -rf pr-worktree/.codex-runtime - mkdir -p pr-worktree/.codex-runtime - cp -R .agents/skills pr-worktree/.codex-runtime/skills + --changed-files-from-diff "${{ steps.handoff.outputs.dir }}/pr_diff.txt" \ + --output "${{ steps.handoff.outputs.dir }}/spec_context.md" - name: Install Codex sandbox prerequisites if: steps.context.outputs.should_run == 'true' && steps.context.outputs.branch_strategy != 'blocked' @@ -202,20 +216,22 @@ jobs: First change directory to pr-worktree and do all file inspection from there. Respond to the PR comment request using these stable local inputs: - - pr_comment_context.json - - pr_event.json - - pr_diff.txt - - spec_context.md when it exists - - review_comment_ids.json - - Read pr_comment_context.json first. If should_run is not true, do - not modify files. In this workflow that case should have been - skipped before Codex runs. - pr_comment_context.json includes the triggering comment body as - trigger_body. pr_event.json includes the pull request title, body, - and metadata. review_comment_ids.json includes current inline review - comment bodies, resolved and outdated thread state, paths, lines, - diff hunks, and URLs. + - ${{ steps.handoff.outputs.dir }}/pr_comment_context.json + - ${{ steps.handoff.outputs.dir }}/pr_event.json + - ${{ steps.handoff.outputs.dir }}/pr_diff.txt + - ${{ steps.handoff.outputs.dir }}/spec_context.md when it exists + - ${{ steps.handoff.outputs.dir }}/review_comment_ids.json + + Read ${{ steps.handoff.outputs.dir }}/pr_comment_context.json + first. If should_run is not true, do not modify files. In this + workflow that case should have been skipped before Codex runs. + ${{ steps.handoff.outputs.dir }}/pr_comment_context.json includes + the triggering comment body as trigger_body. + ${{ steps.handoff.outputs.dir }}/pr_event.json includes the pull + request title, body, and metadata. + ${{ steps.handoff.outputs.dir }}/review_comment_ids.json includes + current inline review comment bodies, resolved and outdated thread + state, paths, lines, diff hunks, and URLs. Read and use these skills in this exact order: 1. .codex-runtime/skills/implement-specs/SKILL.md @@ -229,41 +245,46 @@ jobs: context or call GitHub APIs. Locate the triggering comment or review by trigger_kind and - trigger_comment_id from pr_comment_context.json. Make the smallest - reasonable code, test, documentation, workflow, or spec change that - addresses the request and stays aligned with spec_context.md. + trigger_comment_id from + ${{ steps.handoff.outputs.dir }}/pr_comment_context.json. Make the + smallest reasonable code, test, documentation, workflow, or spec + change that addresses the request and stays aligned with + ${{ steps.handoff.outputs.dir }}/spec_context.md. When the triggering request asks to address all inline comments, unresolved comments, or a category of inline review comments, inspect - review_comment_ids.json and handle every listed inline review - comment that falls within the requested scope. For unresolved - comment requests, only treat comments with is_resolved set to false - as unresolved; do not guess unresolved state from comment text or - missing state. is_outdated only means the original diff position is - stale; do not skip an unresolved outdated comment solely because it - is outdated. Inspect the current code and PR diff to decide whether - the underlying issue still exists. Do not limit the response to - only the triggering comment when the request clearly asks for - multiple comments. + ${{ steps.handoff.outputs.dir }}/review_comment_ids.json and handle + every listed inline review comment that falls within the requested + scope. For unresolved comment requests, only treat comments with + is_resolved set to false as unresolved; do not guess unresolved + state from comment text or missing state. is_outdated only means + the original diff position is stale; do not skip an unresolved + outdated comment solely because it is outdated. Inspect the current + code and PR diff to decide whether the underlying issue still + exists. Do not limit the response to only the triggering comment + when the request clearly asks for multiple comments. Required outputs when you leave implementation changes: - - implementation_summary.md - - pr-metadata.json - - optional resolved_review_comments.json only when this run really - resolves inline review comments listed in review_comment_ids.json - - pr-metadata.json must include branch_name, pr_title, pr_summary, - and intended_files. branch_name must equal - pr_comment_context.json.agent_push_branch. intended_files must list - every intentionally changed repository file and must not include - workflow handoff files, validation logs, generated cache files, or - unchanged files. + - ${{ steps.handoff.outputs.dir }}/implementation_summary.md + - ${{ steps.handoff.outputs.dir }}/pr-metadata.json + - optional ${{ steps.handoff.outputs.dir }}/resolved_review_comments.json + only when this run really resolves inline review comments listed + in ${{ steps.handoff.outputs.dir }}/review_comment_ids.json + + ${{ steps.handoff.outputs.dir }}/pr-metadata.json must include + branch_name, pr_title, pr_summary, and intended_files. branch_name + must equal agent_push_branch from + ${{ steps.handoff.outputs.dir }}/pr_comment_context.json. + intended_files must list every intentionally changed repository + file and must not include workflow handoff files, validation logs, + generated cache files, or unchanged files. When this workflow can push directly to the original PR branch, pr_title and pr_summary are used for validation, commit metadata, and the response comment summary only; the outer workflow must not rewrite the original PR title or body. - resolved_review_comments.json, when present, must contain only - numeric inline review comment ids from review_comment_ids.json and + ${{ steps.handoff.outputs.dir }}/resolved_review_comments.json, + when present, must contain only numeric inline review comment ids + from ${{ steps.handoff.outputs.dir }}/review_comment_ids.json and 1-3 sentence summaries of how this run addressed each comment. If this run resolves multiple inline review comments, include one resolved_review_comments entry for each comment actually resolved. @@ -279,16 +300,6 @@ jobs: working-directory: pr-worktree run: | changed="$(git status --porcelain=v1 --untracked-files=all -- \ - ':!pr_comment_context.json' \ - ':!pr_event.json' \ - ':!review_comment_ids.json' \ - ':!spec_context.md' \ - ':!pr_diff.txt' \ - ':!implementation_summary.md' \ - ':!pr-metadata.json' \ - ':!resolved_review_comments.json' \ - ':!validation-output.txt' \ - ':!validation-error.txt' \ ':!.codex-runtime' \ ':!.codex-runtime/**')" if [ -n "$changed" ]; then @@ -314,12 +325,12 @@ jobs: working-directory: pr-worktree run: | python3 ../.github/scripts/validate_pr_comment_result.py \ - --context pr_comment_context.json \ - --metadata pr-metadata.json \ - --resolved resolved_review_comments.json \ - --review-comment-ids review_comment_ids.json \ - > validation-output.txt \ - 2> validation-error.txt + --context "${{ steps.handoff.outputs.dir }}/pr_comment_context.json" \ + --metadata "${{ steps.handoff.outputs.dir }}/pr-metadata.json" \ + --resolved "${{ steps.handoff.outputs.dir }}/resolved_review_comments.json" \ + --review-comment-ids "${{ steps.handoff.outputs.dir }}/review_comment_ids.json" \ + > "${{ steps.handoff.outputs.dir }}/validation-output.txt" \ + 2> "${{ steps.handoff.outputs.dir }}/validation-error.txt" - name: Check whether workflow update token is required if: steps.context.outputs.should_run == 'true' && steps.context.outputs.branch_strategy != 'blocked' && steps.worktree.outputs.changed == 'true' && steps.validate.outcome == 'success' @@ -329,7 +340,7 @@ jobs: python3 - <<'PY' >> "$GITHUB_OUTPUT" import json - with open("pr-metadata.json", encoding="utf-8") as handle: + with open("${{ steps.handoff.outputs.dir }}/pr-metadata.json", encoding="utf-8") as handle: metadata = json.load(handle) intended_files = metadata.get("intended_files") or [] required = any(str(path).startswith(".github/workflows/") for path in intended_files) @@ -355,8 +366,8 @@ jobs: WORKFLOW_UPDATE_TOKEN: ${{ steps.app-token.outputs.token }} run: | python3 ../.github/scripts/commit_implementation_branch.py \ - --context pr_comment_context.json \ - --metadata pr-metadata.json \ + --context "${{ steps.handoff.outputs.dir }}/pr_comment_context.json" \ + --metadata "${{ steps.handoff.outputs.dir }}/pr-metadata.json" \ --github-output "$GITHUB_OUTPUT" - name: Apply PR comment response result @@ -367,9 +378,9 @@ jobs: run: | python3 .github/scripts/apply_pr_comment_result.py \ --repo "${{ github.repository }}" \ - --context pr-worktree/pr_comment_context.json \ - --metadata pr-worktree/pr-metadata.json \ - --resolved pr-worktree/resolved_review_comments.json \ + --context "${{ steps.handoff.outputs.dir }}/pr_comment_context.json" \ + --metadata "${{ steps.handoff.outputs.dir }}/pr-metadata.json" \ + --resolved "${{ steps.handoff.outputs.dir }}/resolved_review_comments.json" \ --github-output "$GITHUB_OUTPUT" - name: Report no committable PR comment response diff @@ -391,16 +402,13 @@ jobs: with: name: respond-to-pr-comment-${{ github.run_id }} path: | - pr_comment_context.json - pr_event.json - review_comment_ids.json - pr-worktree/pr_comment_context.json - pr-worktree/pr_event.json - pr-worktree/review_comment_ids.json - pr-worktree/pr_diff.txt - pr-worktree/spec_context.md - pr-worktree/implementation_summary.md - pr-worktree/pr-metadata.json - pr-worktree/resolved_review_comments.json - pr-worktree/validation-output.txt - pr-worktree/validation-error.txt + ${{ steps.handoff.outputs.dir }}/pr_comment_context.json + ${{ steps.handoff.outputs.dir }}/pr_event.json + ${{ steps.handoff.outputs.dir }}/review_comment_ids.json + ${{ steps.handoff.outputs.dir }}/pr_diff.txt + ${{ steps.handoff.outputs.dir }}/spec_context.md + ${{ steps.handoff.outputs.dir }}/implementation_summary.md + ${{ steps.handoff.outputs.dir }}/pr-metadata.json + ${{ steps.handoff.outputs.dir }}/resolved_review_comments.json + ${{ steps.handoff.outputs.dir }}/validation-output.txt + ${{ steps.handoff.outputs.dir }}/validation-error.txt diff --git a/docs/github-collaboration-flow.md b/docs/github-collaboration-flow.md index 592ce97..057a317 100644 --- a/docs/github-collaboration-flow.md +++ b/docs/github-collaboration-flow.md @@ -80,9 +80,9 @@ Workflow: 流程: -1. 准备 `issue_context.json` 和 `issue_comments.txt`。 +1. 在 workspace 内 `.codex-runtime/handoff/` 准备 `issue_context.json` 和 `issue_comments.txt`,并把具体路径传给 Codex 和后续脚本。 2. Codex 按顺序使用 `spec-driven-implementation`、`write-product-spec`、`create-product-spec`、`write-tech-spec`、`create-tech-spec`。 -3. 生成 `specs/issue-/product.md`、`specs/issue-/tech.md` 和 `pr-metadata.json`。 +3. 生成 `specs/issue-/product.md`、`specs/issue-/tech.md`,并把 `pr-metadata.json` 写到同一个 `.codex-runtime/handoff/` 目录。 4. 校验输出后推送 `spec/issue-` 分支并创建或更新 spec PR。 Spec PR 只负责规划,不应该实现功能或修改生产代码。 @@ -113,11 +113,11 @@ Workflow: 流程: -1. 准备 `issue_context.json`、`issue_comments.txt`,有 spec context 时生成 `spec_context.md`。 +1. 在 workspace 内 `.codex-runtime/handoff/` 准备 `issue_context.json`、`issue_comments.txt`,有 spec context 时生成 `spec_context.md`。 2. 如果存在关联 spec PR,只有带 `plan-approved` label 的 spec PR 会作为批准的 spec context。 3. 如果发现未批准 spec PR 且默认分支没有 specs,则 workflow noop,并更新 issue progress comment。 4. Codex 按顺序使用 `implement-specs`、`spec-driven-implementation`、`implement-issue`。 -5. Codex 留下实现 diff,并写出 `implementation_summary.md` 和 `pr-metadata.json`。 +5. Codex 留下实现 diff,并把 `implementation_summary.md` 和 `pr-metadata.json` 写到 `.codex-runtime/handoff/`。 6. workflow 校验 metadata,提交并推送实现分支,创建或更新 implementation PR。 目标分支规则: @@ -190,10 +190,10 @@ Workflow: 流程: -1. `prepare_pr_comment_context.py` 生成 `pr_comment_context.json`、`pr_event.json` 和 `review_comment_ids.json`。 -2. workflow checkout PR head,并生成 `pr_diff.txt` 和可选 `spec_context.md`。 +1. `prepare_pr_comment_context.py` 先生成稳定 context,workflow checkout PR head 后把这些 context 放到 `pr-worktree/.codex-runtime/handoff/`。 +2. workflow 在 `pr-worktree/.codex-runtime/handoff/` 生成 `pr_diff.txt` 和可选 `spec_context.md`。 3. Codex 使用 `implement-specs`、`spec-driven-implementation`、`implement-issue`,按触发 comment 的范围做最小修复。 -4. Codex 写出 `implementation_summary.md`、`pr-metadata.json`,必要时写 `resolved_review_comments.json`。 +4. Codex 把 `implementation_summary.md`、`pr-metadata.json`,以及必要时的 `resolved_review_comments.json` 写到 `pr-worktree/.codex-runtime/handoff/`。 5. workflow 校验输出,提交并推送到原 PR 分支或 agent response branch。 6. `apply_pr_comment_result.py` 发布总结,并在有权限和有效 comment id 时处理 resolved review comments。 @@ -224,3 +224,5 @@ resolved_review_comments.json validation-output.txt validation-error.txt ``` + +这些 implementation、spec 和 `/fix` handoff artifact 由 workflow 放在 workspace 内 `.codex-runtime/handoff/` 或 `pr-worktree/.codex-runtime/handoff/` 目录中,artifact 上传仍保留文件名;workflow 的提交和变更检查会排除 `.codex-runtime/`,因此不再依赖仓库根目录 `.gitignore` 来隐藏 scratch 文件。Spec workflow 当前只定义 issue context、comments 和 PR metadata handoff,不定义单独 summary 或 validation-log handoff。 diff --git a/docs/product/raw/implementation-workflow.md b/docs/product/raw/implementation-workflow.md index eab2875..772d909 100644 --- a/docs/product/raw/implementation-workflow.md +++ b/docs/product/raw/implementation-workflow.md @@ -37,13 +37,18 @@ workflow 按固定优先级选择实现上下文: ## Agent 与外层 workflow 职责 -agent 负责读取稳定上下文、产出实现 diff、必要时同步 specs,并写出 -`implementation_summary.md` 与 `pr-metadata.json`。agent 不直接 commit、push、 -创建 PR、更新 PR 或编辑 issue。 +agent 负责读取稳定上下文、产出实现 diff、必要时同步 specs,并把 +`implementation_summary.md` 与 `pr-metadata.json` 写到 workspace 内 +`.codex-runtime/handoff/`。agent 不直接 commit、push、创建 PR、更新 PR 或编辑 +issue。 在 workflow 提供本地上下文文件时,这些文件是该次运行的权威 GitHub context snapshot。 -agent 应优先使用 workflow-provided files,例如 `issue_context.json`、`issue_comments.txt`、 -`pr_comment_context.json`、`review_comment_ids.json`、`pr_diff.txt` 或 `spec_context.md`。 +agent 应优先使用 workflow-provided file paths。Issue implementation workflow 会在 +workspace 内 `.codex-runtime/handoff/` 准备 `issue_context.json`、 +`issue_comments.txt`、可用的 `spec_context.md`、branch SHA snapshot、validation logs +以及 agent 输出的 summary/metadata;PR comment response workflow 会在 +`pr-worktree/.codex-runtime/handoff/` 准备 `pr_comment_context.json`、 +`review_comment_ids.json`、`pr_diff.txt`、可用的 `spec_context.md` 和对应输出。 只有在本地或手动运行缺少完整稳定上下文、且 prompt 明确允许 fetching 时,agent 才能使用 受控的 GitHub context helper 获取额外 issue 或 PR 内容;若认证不可用或 prompt 禁止调用 GitHub API,agent 不应 fetch,并应基于稳定本地上下文继续。 @@ -54,7 +59,7 @@ GitHub API,agent 不应 fetch,并应基于稳定本地上下文继续。 文件;不得包含 workflow handoff 文件、validation logs、生成缓存文件或未变化文件。 当实现需要修改 `.agents` 下的文件,而 Codex sandbox 无法直接写入该目录时,agent 可以把 -完整 replacement 文件写到 `implementation-output/.agents/...` 下,路径必须与目标 +完整 replacement 文件写到 `.codex-runtime/handoff/implementation-output/.agents/...` 下,路径必须与目标 repository-relative path 一致。外层 workflow 会在 diff detection 前应用这些输出文件, 但只接受 `.agents/` 前缀且已列入 `pr-metadata.json` 的 `intended_files` 的路径; `implementation-output` 本身是运行时交接目录,不会作为实现文件提交。 @@ -63,6 +68,8 @@ repository-relative path 一致。外层 workflow 会在 diff detection 前应 implementation PR,并维护 issue progress comment。提交实现分支时,外层 workflow 只提交通过校验且出现在 `intended_files` 中的实现文件;若实际变更与 `intended_files` 不一致,或包含 Python/cache 等生成文件,workflow 会拒绝提交。 +外层 workflow 的变更检查和提交过滤会排除 `.codex-runtime/`,因此这些 handoff 文件不依赖 +仓库根目录 `.gitignore` 来避免污染提交。 当实现变更包含 `.github/workflows/` 下的 GitHub workflow 文件时,外层 workflow 会先根据 `pr-metadata.json` 的 `intended_files` 判断是否需要 workflow 写入权限。只有需要更新 workflow diff --git a/docs/product/raw/pr-comment-response-workflow.md b/docs/product/raw/pr-comment-response-workflow.md index 7222541..0a9d3de 100644 --- a/docs/product/raw/pr-comment-response-workflow.md +++ b/docs/product/raw/pr-comment-response-workflow.md @@ -33,14 +33,15 @@ commit、push、更新 PR、回复评论或 resolve thread。 ## 上下文与安全边界 `prepare_pr_comment_context.py` 是解析 trigger、授权状态、PR 分支信息和分支策略的受控入口。 -它会生成稳定的 `pr_comment_context.json`、PR diff、可用 spec context,以及当前 PR 的 -inline review comment id 索引。context 记录 PR number、head/base repo 与 branch、 +它会生成稳定的 PR comment context。workflow checkout PR head 后,会把 +`pr_comment_context.json`、`pr_event.json`、PR diff、可用 spec context,以及当前 PR 的 +inline review comment id 索引放到 `pr-worktree/.codex-runtime/handoff/`。context 记录 PR number、head/base repo 与 branch、 trigger metadata、触发者授权状态、branch strategy、agent push 目标、coauthor directives, 以及触发评论正文 `trigger_body`。context 还会暴露 `base_repo_private` 和 `trigger_actor_repository_permission`,用于说明私有仓库 `CONTRIBUTOR` fallback 授权判断 或拒绝原因。 -agent 使用 workflow 提供的稳定本地快照作为 PR 讨论上下文:`pr_comment_context.json`、 +agent 使用 workflow 在 `pr-worktree/.codex-runtime/handoff/` 提供的稳定本地快照作为 PR 讨论上下文:`pr_comment_context.json`、 `pr_event.json`、`pr_diff.txt`、可用的 `spec_context.md` 和 `review_comment_ids.json`。 `pr_event.json` 包含 PR title、body 和 metadata;`review_comment_ids.json` 包含当前 inline review comments 的 bodies、resolved/outdated thread state、paths、lines、diff hunks 和 URLs。 @@ -75,19 +76,23 @@ workflow 使用三种分支策略: Agent 根据触发评论、PR diff、相关讨论和 spec context 做最小合理修改。没有值得提交的 diff 时, workflow 不创建空提交,也不创建 follow-up PR。 -有可提交 diff 时,agent 必须写出 `implementation_summary.md` 和 `pr-metadata.json`。 +有可提交 diff 时,agent 必须把 `implementation_summary.md` 和 `pr-metadata.json` 写到 +`pr-worktree/.codex-runtime/handoff/`。 `pr-metadata.json` 至少包含目标 `branch_name`、`pr_title`、`pr_summary` 和 `intended_files`;`branch_name` 必须等于 context 中允许的 `agent_push_branch`。 `intended_files` 必须覆盖所有应提交的 repository-relative 文件,不能包含 handoff、日志或缓存文件。 当本次修改确实解决 inline review comments 时,agent 可以写出 -`resolved_review_comments.json`。其中每个 `comment_id` 必须来自当前 PR 真实 inline +`pr-worktree/.codex-runtime/handoff/resolved_review_comments.json`。其中每个 `comment_id` 必须来自当前 PR 真实 inline review comment id,不能使用普通 conversation comment id、review id、其他 PR 的 comment id 或编造 id。没有解决 inline review comment 时不应上传该文件。当一次运行实际解决多条 inline review comments 时,`resolved_review_comments.json` 必须为每条已解决 comment 分别包含一条 entry。 外层 workflow 校验 metadata、resolved comments 和实际 diff 后提交并 push 到允许 branch。 +外层 workflow 的变更检查和提交过滤会排除 `pr-worktree/.codex-runtime/`,因此 context、 +metadata、summary、validation logs 和 resolved-comments handoff 不会进入提交,也不需要 +仓库根目录 `.gitignore` 覆盖。 `push-head` 成功后不会按 metadata 改写原 PR title/body;`pr_title` 与 `pr_summary` 仍用于校验、commit metadata 和回复评论摘要。`fallback-pr-to-fork` 成功后会查找或创建 follow-up PR,并在 PR body 中说明来源 PR 和触发评论。无论是更新原 PR 分支还是创建 diff --git a/docs/product/raw/spec-workflow.md b/docs/product/raw/spec-workflow.md index 06a366d..76c5e34 100644 --- a/docs/product/raw/spec-workflow.md +++ b/docs/product/raw/spec-workflow.md @@ -50,5 +50,14 @@ open、draft 与同仓库 head 条件决定。 创建或更新 PR 时,workflow 只复用同一 head branch 上的 open PR;不会把 closed PR 当作可更新目标。 +## Agent handoff + +Create-spec workflow 在 workspace 内 `.codex-runtime/handoff/` 准备 +`issue_context.json` 和 `issue_comments.txt`,并要求 spec agent 把 +`pr-metadata.json` 写到同一目录。agent 直接修改目标 `product.md` 与 `tech.md` +spec 文件;当前 spec workflow 不定义单独的 summary handoff,也不上传 validation log +handoff。外层 workflow 校验 `pr-metadata.json` 后创建或更新 spec PR,并排除 +`.codex-runtime/`,因此这些 handoff 文件不依赖仓库根目录 `.gitignore`。 + 来源:PR #56,PR #58,PR #65,PR #66,PR #67,PR #74,PR #82,PR #84, `specs/issue-77/product.md`。 diff --git a/docs/product/wiki/concepts/agent-workflow-boundaries.md b/docs/product/wiki/concepts/agent-workflow-boundaries.md index 4eb10ef..ecd9964 100644 --- a/docs/product/wiki/concepts/agent-workflow-boundaries.md +++ b/docs/product/wiki/concepts/agent-workflow-boundaries.md @@ -33,14 +33,16 @@ AICodingFlow 的自动化把 agent 的代码或文档产出职责,与外层 Gi - Spec workflow 由外层 GitHub Actions 创建或更新 spec PR。 - `create-pr` skill 准备 PR 标题、正文、base/head 信息和 issue 关联;不负责实现代码、提交、推送分支或修改 GitHub issue。 - Implementation workflow 由外层 GitHub Actions 创建或更新 implementation PR。 -- Implementation agent 负责读取稳定上下文、产出实现 diff、必要时同步 specs,并写出 `implementation_summary.md` 与 `pr-metadata.json`。 +- Implementation agent 负责读取 workflow 提供的稳定上下文路径、产出实现 diff、必要时同步 specs,并把 `implementation_summary.md` 与 `pr-metadata.json` 写到 workflow 指定的 `.codex-runtime/handoff/` 目录。 - Implementation agent 不直接 commit、push、创建 PR、更新 PR 或编辑 issue。 - Implementation 外层 workflow 负责校验 agent metadata,提交并推送目标分支,创建或更新 implementation PR,并维护 issue progress comment。 - `pr-metadata.json` 必须包含 `branch_name`、`pr_title`、`pr_summary` 和 `intended_files`。 - `intended_files` 必须覆盖所有有意修改的 production、test、spec、`.agents` 或 workflow 文件。 - 外层 workflow 只提交通过校验且出现在 `intended_files` 中的实现文件;若实际变更与 `intended_files` 不一致,或包含 Python/cache 等生成文件,workflow 会拒绝提交。 +- Spec workflow 的 `issue_context.json`、`issue_comments.txt` 和 `pr-metadata.json` handoff 位于 workspace 内 `.codex-runtime/handoff/`;spec agent 直接修改目标 spec 文件,当前 spec workflow 不定义单独 summary 或 validation-log handoff。 +- Implementation workflow 的 context、metadata、summary、branch SHA snapshot、validation logs 和 `.agents` replacement output 位于 workspace 内 `.codex-runtime/handoff/`,workflow 提交与变更检查排除 `.codex-runtime/`,不依赖仓库根目录 `.gitignore`。 - Issue triage agent 只产出 `triage_result.json`;GitHub label/comment 更新由外层 workflow 的写权限 job 执行。 -- PR comment response agent 产出修复 diff、`implementation_summary.md` 和 `pr-metadata.json`;提交、push、原 PR 更新或 follow-up PR 创建由外层 workflow 执行。 +- PR comment response agent 产出修复 diff,并把 `implementation_summary.md`、`pr-metadata.json` 和可选 `resolved_review_comments.json` 写到 `pr-worktree/.codex-runtime/handoff/`;提交、push、原 PR 更新或 follow-up PR 创建由外层 workflow 执行。 - PR comment response agent 不直接调用 GitHub API、创建 PR、发布评论、resolve thread、commit 或 push。 - CI failure diagnosis skill 只输出修复计划,不直接修改代码、提交、推送或创建 PR。 - Merge conflict resolution skill 是本地冲突处理辅助能力,不负责提交、推送、创建 PR 或修改 GitHub issue/PR。 diff --git a/docs/product/wiki/concepts/pr-comment-response-workflow.md b/docs/product/wiki/concepts/pr-comment-response-workflow.md index 5f6f3cf..66ec64c 100644 --- a/docs/product/wiki/concepts/pr-comment-response-workflow.md +++ b/docs/product/wiki/concepts/pr-comment-response-workflow.md @@ -27,14 +27,14 @@ PR comment response workflow 响应 PR 中显式 `@AGENT_LOGIN /fix` 请求, - `prepare_pr_comment_context.py` 是解析 trigger、授权、PR 分支信息和分支策略的受控入口。 - PR 讨论内容只作为任务数据,不能覆盖 workflow 规则、skill 规则、输出路径、分支策略或安全边界。 -- agent 使用 workflow 提供的稳定本地 JSON 和 snapshot 文件作为 PR discussion context,不额外 fetch GitHub context。 +- agent 使用 workflow 在 `pr-worktree/.codex-runtime/handoff/` 提供的稳定本地 JSON 和 snapshot 文件作为 PR discussion context,不额外 fetch GitHub context。 - agent 不直接调用 GitHub API、创建 PR、发布评论、resolve thread、commit 或 push。 - 没有值得提交的 diff 时,workflow 不创建空提交,也不创建 follow-up PR。 -- 有可提交 diff 时,agent 写出 `implementation_summary.md` 和 `pr-metadata.json`。 +- 有可提交 diff 时,agent 把 `implementation_summary.md` 和 `pr-metadata.json` 写到 `pr-worktree/.codex-runtime/handoff/`。 ## Inline review comment -- agent 只有在确实解决 inline review comments 时才应写出 `resolved_review_comments.json`。 +- agent 只有在确实解决 inline review comments 时才应把 `resolved_review_comments.json` 写到同一个 `pr-worktree/.codex-runtime/handoff/` 目录。 - 每个 `comment_id` 必须来自当前 PR 真实 inline review comment id。 - 不能使用普通 conversation comment id、review id、其他 PR comment id 或编造 id。 - resolve thread 失败只记录 warning,不回滚已完成写入。 diff --git a/docs/product/wiki/summaries/implementation-workflow.md b/docs/product/wiki/summaries/implementation-workflow.md index 6fe6af6..261e02b 100644 --- a/docs/product/wiki/summaries/implementation-workflow.md +++ b/docs/product/wiki/summaries/implementation-workflow.md @@ -38,12 +38,13 @@ Source: [docs/product/raw/implementation-workflow.md](../../raw/implementation-w ## 职责边界 -- agent 负责读取稳定上下文、产出实现 diff、必要时同步 specs,并写出 `implementation_summary.md` 与 `pr-metadata.json`。 +- agent 负责读取 workflow 提供的稳定上下文路径、产出实现 diff、必要时同步 specs,并把 `implementation_summary.md` 与 `pr-metadata.json` 写到 workspace 内 `.codex-runtime/handoff/`。 - agent 不直接 commit、push、创建 PR、更新 PR 或编辑 issue。 - 外层 workflow 负责校验 metadata、提交并推送目标分支、创建或更新 implementation PR,并维护 issue progress comment。 - `pr-metadata.json` 必须包含 `branch_name`、`pr_title`、`pr_summary` 和 `intended_files`。 - `intended_files` 是外层 workflow 应提交的 repository-relative 实现文件列表,必须覆盖所有有意修改的 production、test、spec、`.agents` 或 workflow 文件。 - `intended_files` 不得包含 workflow handoff 文件、validation logs、生成缓存文件或未变化文件;实际变更与 `intended_files` 不一致时,workflow 会拒绝提交。 +- context、metadata、summary、branch SHA snapshot 与 validation logs 位于 workspace 内 `.codex-runtime/handoff/`,workflow 排除该目录,不依赖仓库根目录 `.gitignore`。 - implementation 变更包含 `.github/workflows/` 下 GitHub workflow 文件时,外层 workflow 会通过 `actions/create-github-app-token` 生成短期 GitHub App installation token,并作为 `WORKFLOW_UPDATE_TOKEN` 传给提交脚本。 - 仓库需要配置 `APP_CLIENT_ID` Actions variable 和 `APP_PRIVATE_KEY` Actions secret;对应 GitHub App 必须安装到目标仓库,并具有 `Contents: Read and write` 与 `Workflows: Read and write` 权限。 - 生成出来的一次性 installation token 不应存成 secret。 diff --git a/docs/product/wiki/summaries/pr-comment-response-workflow.md b/docs/product/wiki/summaries/pr-comment-response-workflow.md index 2380302..08f1ea9 100644 --- a/docs/product/wiki/summaries/pr-comment-response-workflow.md +++ b/docs/product/wiki/summaries/pr-comment-response-workflow.md @@ -41,11 +41,12 @@ Source: [docs/product/raw/pr-comment-response-workflow.md](../../raw/pr-comment- ## Agent 输出与外层处理 - agent 做最小合理修改;没有值得提交的 diff 时,workflow 不创建空提交或 follow-up PR。 -- 有可提交 diff 时,agent 必须写出 `implementation_summary.md` 和 `pr-metadata.json`。 +- 有可提交 diff 时,agent 必须把 `implementation_summary.md` 和 `pr-metadata.json` 写到 `pr-worktree/.codex-runtime/handoff/`。 - `pr-metadata.json` 至少包含 `branch_name`、`pr_title`、`pr_summary` 和 `intended_files`;`branch_name` 必须等于 context 允许的 `agent_push_branch`。 - `intended_files` 必须覆盖所有应提交文件,不能包含 handoff、日志或缓存文件。 -- agent 可在确实解决 inline review comments 时写出 `resolved_review_comments.json`,其中 comment id 必须来自当前 PR 真实 inline review comment id。 +- agent 可在确实解决 inline review comments 时把 `resolved_review_comments.json` 写到同一个 `pr-worktree/.codex-runtime/handoff/` 目录,其中 comment id 必须来自当前 PR 真实 inline review comment id。 - 外层 workflow 校验 metadata、resolved comments 和实际 diff 后提交并 push 到允许 branch。 +- PR discussion context、diff snapshot、spec context、metadata、summary 和 validation logs 位于 `pr-worktree/.codex-runtime/handoff/`,workflow 排除 `.codex-runtime/`,不污染提交内容。 - `push-head` 成功后不会按 metadata 改写原 PR title/body;fallback PR 会在 PR body 中说明来源 PR 和触发评论。 - resolve review thread 失败只记录 warning,不回滚已完成的 commit、push 或 PR update。 - 修改 `.github/workflows/` 下 GitHub workflow 文件时,外层 workflow 会通过 GitHub App installation token 设置 `WORKFLOW_UPDATE_TOKEN`;仓库需要配置 `APP_CLIENT_ID` Actions variable 和 `APP_PRIVATE_KEY` Actions secret。