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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 9 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
The MVP is Codex-first:

- reads Codex hook JSON from stdin;
- captures only explicit plan blocks such as `<proposed_plan>...</proposed_plan>` or `## Accepted Plan`;
- captures only explicit plan blocks such as `<proposed_plan>...</proposed_plan>`, `<proposed_plan title="...">...</proposed_plan>`, 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 a PR exists;
- leaves the local stack queued when no PR exists yet.
- 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.

## CLI

Expand Down Expand Up @@ -40,22 +40,24 @@ type = "command"
command = "plan-to-git hook --source codex"
```

Exact hook configuration shape can vary by Codex release. The hook command itself expects the release behavior documented by Codex hooks: `Stop` includes `last_assistant_message`, and `UserPromptSubmit` includes `prompt`.
Exact hook configuration shape can vary by Codex release. The hook command itself expects the release behavior documented by Codex hooks: `Stop` includes the final agent message (`last_agent_message`, with `last_assistant_message` still accepted for older payloads), and `UserPromptSubmit` includes `prompt`.

If an agent emits known XML-style plan sections (`summary`, `flow`, `test_plan`, or `assumptions`) inside a proposed plan, `plan-to-git` normalizes them to Markdown headings before storage and PR sync.

## Pull Request Comments

When `gh pr view` finds a 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 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

...
```

The PR description is not edited. 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 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.

## Safety

The hook path only uses stable hook payload fields and explicitly marked plan text. `import-codex` can backfill previous plans from `~/.codex/sessions`, but it only reads assistant message events from sessions that match the current repository and branch, and it still imports only explicit markers such as `<proposed_plan>...</proposed_plan>` or `## Accepted Plan`.
The hook path only uses stable hook payload fields and explicitly marked plan text. `import-codex` can backfill previous plans from `~/.codex/sessions`, but it only reads assistant message events from sessions that match the current repository and branch, and it still imports only explicit markers such as `<proposed_plan>...</proposed_plan>`, `<proposed_plan title="...">...</proposed_plan>`, or `## Accepted Plan`.

Captured content is redacted before local storage and PR sync. `.agent-plan.json` also acts as the sent-plan registry: content hashes prevent the same plan from being added and commented again.
9 changes: 9 additions & 0 deletions changelog.d/20260603_195500_guard_plan_sync_closed_prs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
bump: patch
---

### Fixed
- Kept captured plan updates queued instead of posting comments to closed or merged pull requests.

### Changed
- Accepted current Codex `last_agent_message` hook payloads and normalized known XML-style plan sections before PR sync.
2 changes: 1 addition & 1 deletion src/capture.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ struct CodexHookInput {
turn_id: Option<String>,
#[serde(default)]
prompt: Option<String>,
#[serde(default)]
#[serde(default, alias = "last_agent_message")]
last_assistant_message: Option<String>,
}

Expand Down
13 changes: 12 additions & 1 deletion src/github.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ use crate::store::AgentPlanState;
pub enum SyncStatus {
NoItems,
NoPullRequest,
ClosedPullRequest {
number: u64,
state: String,
},
Unchanged {
number: u64,
},
Expand All @@ -27,6 +31,7 @@ pub enum SyncStatus {
#[derive(Debug, Deserialize)]
struct PullRequest {
number: u64,
state: String,
}

#[derive(Debug, Deserialize)]
Expand All @@ -42,6 +47,12 @@ pub fn sync_state(context: &GitContext, state: &mut AgentPlanState) -> AppResult
let Some(pull_request) = view_current_pr(&context.repo_root)? else {
return Ok(SyncStatus::NoPullRequest);
};
if !pull_request.state.eq_ignore_ascii_case("OPEN") {
return Ok(SyncStatus::ClosedPullRequest {
number: pull_request.number,
state: pull_request.state,
});
}

let (comment_body, item_ids, item_count) = {
let items = state.unposted_items_for_pr(pull_request.number);
Expand All @@ -67,7 +78,7 @@ pub fn sync_state(context: &GitContext, state: &mut AgentPlanState) -> AppResult
fn view_current_pr(repo_root: &Path) -> AppResult<Option<PullRequest>> {
let output = Command::new("gh")
.current_dir(repo_root)
.args(["pr", "view", "--json", "number"])
.args(["pr", "view", "--json", "number,state,url"])
.output()?;

if output.status.success() {
Expand Down
3 changes: 3 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,9 @@ fn print_sync_status(status: &SyncStatus) {
match status {
SyncStatus::NoItems => println!("no captured plan items to sync"),
SyncStatus::NoPullRequest => println!("no pull request found for the current branch"),
SyncStatus::ClosedPullRequest { number, state } => {
println!("pull request #{number} is {state}; leaving plan items queued");
}
SyncStatus::Unchanged { number } => {
println!("no new plan items to comment on pull request #{number}");
}
Expand Down
Loading
Loading