From 87dd39d127f4d15b62591819331c731216f62568 Mon Sep 17 00:00:00 2001 From: konard Date: Tue, 9 Jun 2026 20:27:54 +0000 Subject: [PATCH 1/3] Initial commit with task details Adding .gitkeep for PR creation (default mode). This file will be removed when the task is complete. Issue: https://github.com/ProverCoderAI/plan-to-git/issues/6 --- .gitkeep | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitkeep b/.gitkeep index a81614e..920eb52 100644 --- a/.gitkeep +++ b/.gitkeep @@ -1 +1,2 @@ -# .gitkeep file auto-generated at 2026-05-12T22:59:34.075Z for PR creation at branch issue-50-f6272907aac6 for issue https://github.com/link-foundation/rust-ai-driven-development-pipeline-template/issues/50 \ No newline at end of file +# .gitkeep file auto-generated at 2026-05-12T22:59:34.075Z for PR creation at branch issue-50-f6272907aac6 for issue https://github.com/link-foundation/rust-ai-driven-development-pipeline-template/issues/50 +# Updated: 2026-06-09T20:27:54.325Z \ No newline at end of file From 842b74f35011e9a05fc8ba512ad0c0a2124fac19 Mon Sep 17 00:00:00 2001 From: konard Date: Wed, 10 Jun 2026 10:24:14 +0000 Subject: [PATCH 2/3] fix: guard plan sync for draft pull requests Keep captured plan items queued on the upload stack until the pull request is valid (open and ready for review). A draft PR has state "OPEN" but isDraft=true; previously plan-update comments were posted to draft PRs. Now sync queries isDraft, reports a DraftPullRequest status, and leaves items queued so they are not dropped before a valid PR exists. Fixes #6 --- README.md | 8 +- ...260610_000000_guard_plan_sync_draft_prs.md | 6 ++ src/github.rs | 12 ++- src/main.rs | 3 + tests/integration/cli.rs | 98 ++++++++++++++++++- 5 files changed, 119 insertions(+), 8 deletions(-) create mode 100644 changelog.d/20260610_000000_guard_plan_sync_draft_prs.md diff --git a/README.md b/README.md index b2bf34c..4ce5ed0 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,8 @@ The MVP is Codex-first: - reads Codex hook JSON from stdin; - captures only explicit plan blocks such as `...`, `...`, or `## Accepted Plan`; - stores captured plans and planning Q/A decisions in `.agent-plan.json`; -- posts a new PR comment with newly captured current-branch items when an open PR exists; -- leaves the local stack queued when no open PR exists yet. +- posts a new PR comment with newly captured current-branch items when a valid (open, non-draft) PR exists; +- leaves the local stack queued when no valid PR exists yet. ## CLI @@ -46,7 +46,7 @@ If an agent emits known XML-style plan sections (`summary`, `flow`, `test_plan`, ## Pull Request Comments -When `gh pr view` finds an open PR for the current branch, `plan-to-git` creates a new issue comment on that PR containing items that have not been posted before: +When `gh pr view` finds an open, non-draft PR for the current branch, `plan-to-git` creates a new issue comment on that PR containing items that have not been posted before: ```markdown ## Agent Plan Update @@ -54,7 +54,7 @@ When `gh pr view` finds an open PR for the current branch, `plan-to-git` creates ... ``` -The PR description is not edited. Closed or merged pull requests are not commented on; new items stay queued until an open PR exists. After a comment is created, `.agent-plan.json` records the posted item hashes and GitHub comment id so repeated `sync`, `hook`, or `import-codex` runs do not post the same plan again, including on a later PR. +The PR description is not edited. Closed, merged, or still-draft pull requests are not commented on; new items stay queued until the PR is valid (open and marked ready for review). After a comment is created, `.agent-plan.json` records the posted item hashes and GitHub comment id so repeated `sync`, `hook`, or `import-codex` runs do not post the same plan again, including on a later PR. ## Safety diff --git a/changelog.d/20260610_000000_guard_plan_sync_draft_prs.md b/changelog.d/20260610_000000_guard_plan_sync_draft_prs.md new file mode 100644 index 0000000..9bd1241 --- /dev/null +++ b/changelog.d/20260610_000000_guard_plan_sync_draft_prs.md @@ -0,0 +1,6 @@ +--- +bump: patch +--- + +### Fixed +- Kept captured plan updates queued instead of posting comments to draft pull requests, so plans stay on the upload stack until the PR is valid (open and ready for review). diff --git a/src/github.rs b/src/github.rs index bcda595..8e2fe9f 100644 --- a/src/github.rs +++ b/src/github.rs @@ -18,6 +18,9 @@ pub enum SyncStatus { number: u64, state: String, }, + DraftPullRequest { + number: u64, + }, Unchanged { number: u64, }, @@ -32,6 +35,8 @@ pub enum SyncStatus { struct PullRequest { number: u64, state: String, + #[serde(default, rename = "isDraft")] + is_draft: bool, } #[derive(Debug, Deserialize)] @@ -53,6 +58,11 @@ pub fn sync_state(context: &GitContext, state: &mut AgentPlanState) -> AppResult state: pull_request.state, }); } + if pull_request.is_draft { + return Ok(SyncStatus::DraftPullRequest { + number: pull_request.number, + }); + } let (comment_body, item_ids, item_count) = { let items = state.unposted_items_for_pr(pull_request.number); @@ -78,7 +88,7 @@ pub fn sync_state(context: &GitContext, state: &mut AgentPlanState) -> AppResult fn view_current_pr(repo_root: &Path) -> AppResult> { let output = Command::new("gh") .current_dir(repo_root) - .args(["pr", "view", "--json", "number,state,url"]) + .args(["pr", "view", "--json", "number,state,url,isDraft"]) .output()?; if output.status.success() { diff --git a/src/main.rs b/src/main.rs index dceeed7..656b2da 100644 --- a/src/main.rs +++ b/src/main.rs @@ -190,6 +190,9 @@ fn print_sync_status(status: &SyncStatus) { SyncStatus::ClosedPullRequest { number, state } => { println!("pull request #{number} is {state}; leaving plan items queued"); } + SyncStatus::DraftPullRequest { number } => { + println!("pull request #{number} is a draft; leaving plan items queued"); + } SyncStatus::Unchanged { number } => { println!("no new plan items to comment on pull request #{number}"); } diff --git a/tests/integration/cli.rs b/tests/integration/cli.rs index 1e32d4a..aaed595 100644 --- a/tests/integration/cli.rs +++ b/tests/integration/cli.rs @@ -296,6 +296,77 @@ mod unix { assert!(!captured_request.exists()); } + #[test] + fn hook_leaves_plans_queued_when_pr_is_draft() { + let temp_dir = tempfile::tempdir().expect("temp dir"); + let bin_dir = temp_dir.path().join("bin"); + let repo_dir = temp_dir.path().join("repo"); + let captured_request = temp_dir.path().join("request.json"); + fs::create_dir_all(&bin_dir).expect("bin dir"); + fs::create_dir_all(&repo_dir).expect("repo dir"); + write_fake_git(&bin_dir, &repo_dir); + write_fake_gh_draft_pr(&bin_dir, &captured_request); + + run_hook( + &repo_dir, + &bin_dir, + &format!( + r#"{{ + "session_id":"session", + "cwd":"{}", + "hook_event_name":"Stop", + "turn_id":"turn", + "last_assistant_message":"\n# Queued\n\n- Wait for a valid PR\n" + }}"#, + repo_dir.display() + ), + ); + + let state = fs::read_to_string(repo_dir.join(STATE_FILE_NAME)).expect("state file"); + assert!(state.contains("Wait for a valid PR")); + assert!(state.contains("\"posted_comments\": []")); + assert!(!captured_request.exists()); + } + + #[test] + fn sync_reports_draft_pr_and_does_not_comment() { + let temp_dir = tempfile::tempdir().expect("temp dir"); + let bin_dir = temp_dir.path().join("bin"); + let repo_dir = temp_dir.path().join("repo"); + let captured_request = temp_dir.path().join("request.json"); + fs::create_dir_all(&bin_dir).expect("bin dir"); + fs::create_dir_all(&repo_dir).expect("repo dir"); + write_fake_git(&bin_dir, &repo_dir); + write_fake_gh_draft_pr(&bin_dir, &captured_request); + + run_hook( + &repo_dir, + &bin_dir, + &format!( + r#"{{ + "session_id":"session", + "cwd":"{}", + "hook_event_name":"Stop", + "turn_id":"turn", + "last_assistant_message":"\n# Queued\n\n- Wait for a valid PR\n" + }}"#, + repo_dir.display() + ), + ); + + let output = Command::new(env!("CARGO_BIN_EXE_plan-to-git")) + .arg("sync") + .current_dir(&repo_dir) + .env("PATH", path_with_fake_bin(&bin_dir)) + .output() + .expect("run sync"); + + assert!(output.status.success()); + let stdout = String::from_utf8(output.stdout).expect("stdout"); + assert!(stdout.contains("pull request #17 is a draft; leaving plan items queued")); + assert!(!captured_request.exists()); + } + #[test] fn sync_reports_merged_pr_and_does_not_comment() { let temp_dir = tempfile::tempdir().expect("temp dir"); @@ -441,7 +512,7 @@ esac fn write_fake_gh_no_pr(bin_dir: &Path) { let script = r#"#!/usr/bin/env bash set -euo pipefail -if [[ "$*" == "pr view --json number,state,url" ]]; then +if [[ "$*" == "pr view --json number,state,url,isDraft" ]]; then echo 'no pull requests found for branch "feature/test"' >&2 exit 1 fi @@ -455,7 +526,7 @@ exit 1 let script = format!( r#"#!/usr/bin/env bash set -euo pipefail -if [[ "$*" == "pr view --json number,state,url" ]]; then +if [[ "$*" == "pr view --json number,state,url,isDraft" ]]; then printf '%s\n' '{{"number":17,"state":"OPEN","url":"https://github.com/example/repo/pull/17"}}' exit 0 fi @@ -476,7 +547,7 @@ exit 1 let script = format!( r#"#!/usr/bin/env bash set -euo pipefail -if [[ "$*" == "pr view --json number,state,url" ]]; then +if [[ "$*" == "pr view --json number,state,url,isDraft" ]]; then printf '%s\n' '{{"number":17,"state":"{state}","url":"https://github.com/example/repo/pull/17"}}' exit 0 fi @@ -493,6 +564,27 @@ exit 1 write_executable(&bin_dir.join("gh"), &script); } + fn write_fake_gh_draft_pr(bin_dir: &Path, captured_request: &Path) { + let script = format!( + r#"#!/usr/bin/env bash +set -euo pipefail +if [[ "$*" == "pr view --json number,state,url,isDraft" ]]; then + printf '%s\n' '{{"number":17,"state":"OPEN","url":"https://github.com/example/repo/pull/17","isDraft":true}}' + exit 0 +fi +if [[ "$1" == "api" ]]; then + printf '%s\n' "$*" > "{}" + echo "comment API should not be called for draft PR" >&2 + exit 1 +fi +echo "unexpected gh args: $*" >&2 +exit 1 +"#, + captured_request.display() + ); + write_executable(&bin_dir.join("gh"), &script); + } + fn write_executable(path: &Path, content: &str) { fs::write(path, content).expect("write script"); let mut permissions = fs::metadata(path).expect("metadata").permissions(); From 2e29cafed2476848b2e95d920332a214adbe718b Mon Sep 17 00:00:00 2001 From: konard Date: Wed, 10 Jun 2026 10:28:23 +0000 Subject: [PATCH 3/3] Revert "Initial commit with task details" This reverts commit 87dd39d127f4d15b62591819331c731216f62568. --- .gitkeep | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.gitkeep b/.gitkeep index 920eb52..a81614e 100644 --- a/.gitkeep +++ b/.gitkeep @@ -1,2 +1 @@ -# .gitkeep file auto-generated at 2026-05-12T22:59:34.075Z for PR creation at branch issue-50-f6272907aac6 for issue https://github.com/link-foundation/rust-ai-driven-development-pipeline-template/issues/50 -# Updated: 2026-06-09T20:27:54.325Z \ No newline at end of file +# .gitkeep file auto-generated at 2026-05-12T22:59:34.075Z for PR creation at branch issue-50-f6272907aac6 for issue https://github.com/link-foundation/rust-ai-driven-development-pipeline-template/issues/50 \ No newline at end of file