Skip to content
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,25 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.3.0] - 2026-08-25

**Scope.** This release promotes `v1.2.0..c095d5d`: 6 commits and 3 first-parent
units, measured with `git rev-list --count` and `git rev-list --first-parent --count`
over that exact range. All three units change the published Rust CLI.

### Added
- **Codex startup prompts use the native developer boundary.** `gr spawn` loads each
configured `startup_prompt` and supplies it as `developer_instructions`, while recall
continuity remains a separate optional user payload. Missing configured prompts fail
closed. Generated launch scripts are constructed on mode-0600 inodes and atomically
replace prior files.

### Fixed
- **Unknown repository filters are refused** by `gr add`, `gr commit`, and `gr push`
instead of matching zero repositories and reporting success.
- **Pruning protects the remote default branch** in addition to the current and target
branches.

## [1.2.0] - 2026-08-21

**Scope, stated first because the range and the artifact are not the same thing.**
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "gitgrip"
version = "1.2.0"
version = "1.3.0"
edition = "2021"
rust-version = "1.80"
description = "Multi-repo workflow tool - manage multiple git repositories as one"
Expand Down Expand Up @@ -99,11 +99,11 @@ urlencoding = "2"
base64 = "0.22"
futures = "0.3"
which = "7"
tempfile = "3"
clap_complete = "4.5.65"
rusqlite = { version = "0.39.0", features = ["bundled"] }

[dev-dependencies]
tempfile = "3"
tokio-test = "0.4"
wiremock = "0.6"
assert_cmd = "2"
Expand Down
12 changes: 8 additions & 4 deletions docs/gr-spawn.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,12 @@ Each agent can have a startup prompt file (Markdown) that defines its role, resp
reviewer.md
```

Prompts are injected via the `--prompt` flag when launching the agent's CLI tool. They should include:
Startup prompt delivery follows the target runtime's native instruction boundary. For
Codex, gitgrip passes the file as `developer_instructions`, keeping identity separate
from the first user turn. Other tools can consume the same file through their configured
launch arguments, such as Claude Code's `--append-system-prompt-file`.

Prompts should include:
- Role description
- Responsibilities
- Startup checklist (join channel, read journal, check unread, start loop)
Expand Down Expand Up @@ -140,7 +145,7 @@ tmux select-window -t myproject:lead
|-------|---------|-------------|
| `session_name` | `"synapt"` | tmux session name |
| `channel` | `"dev"` | Default channel all agents join |
| `auto_journal` | `true` | Agents read `recall_journal` on startup |
| `auto_journal` | `true` | Paste recall startup context into Codex as a compatibility fallback. Disable when a runtime SessionStart hook owns continuity. |
| `mock_launch` | `false` | Use echo/sleep instead of real agent launch |

### [agents.*] section
Expand All @@ -151,7 +156,7 @@ tmux select-window -t myproject:lead
| `model` | no | `claude-sonnet-4-6` | Model ID |
| `tool` | no | `claude` | CLI tool (`claude`, `codex`, `cursor`) |
| `worktree` | no | `main` | Git worktree or `"new"` to auto-create |
| `startup_prompt` | no | — | Path to .md startup prompt file |
| `startup_prompt` | no | — | Path to an agent identity prompt. Codex receives it as developer instructions. |
| `channel` | no | from `[spawn]` | Channel to auto-join |
| `loop_interval` | no | `5m` | Channel read loop cadence |
| `heartbeat_interval` | no | `60` | Seconds between heartbeat pings |
Expand All @@ -174,4 +179,3 @@ tmux select-window -t myproject:lead
| `SYNAPT_LOOP_INTERVAL` | `loop_interval` field | `2m` |

Plus any custom vars from the `env` table in `agents.toml`.

6 changes: 5 additions & 1 deletion src/cli/commands/add.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
//! Add command implementation

use crate::cli::outcome::CliOutcomeError;
use crate::cli::output::Output;
use crate::core::manifest::Manifest;
use crate::core::manifest_paths;
use crate::core::repo::{filter_repos, RepoInfo};
use crate::core::repo::{filter_repos, validate_repo_filters_known, RepoInfo};
use crate::git::cache::invalidate_status_cache;
use crate::git::{get_workdir, open_repo, path_exists};
use crate::util::log_cmd;
Expand All @@ -19,6 +20,9 @@ pub fn run_add(
repos_filter: Option<&[String]>,
group_filter: Option<&[String]>,
) -> anyhow::Result<()> {
validate_repo_filters_known(manifest, repos_filter)
.map_err(|error| CliOutcomeError::refusal(error.to_string()))?;

Output::header("Checking repositories for changes to stage...");
println!();

Expand Down
6 changes: 5 additions & 1 deletion src/cli/commands/commit.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
//! Commit command implementation

use crate::cli::outcome::CliOutcomeError;
use crate::cli::output::Output;
use crate::core::manifest::Manifest;
use crate::core::manifest_paths;
use crate::core::repo::{filter_repos, RepoInfo};
use crate::core::repo::{filter_repos, validate_repo_filters_known, RepoInfo};
use crate::git::cache::invalidate_status_cache;
use crate::git::{get_workdir, open_repo, path_exists};
use crate::util::log_cmd;
Expand All @@ -22,6 +23,9 @@ pub fn run_commit(
repos_filter: Option<&[String]>,
group_filter: Option<&[String]>,
) -> anyhow::Result<()> {
validate_repo_filters_known(manifest, repos_filter)
.map_err(|error| CliOutcomeError::refusal(error.to_string()))?;

if !json {
Output::header("Committing changes...");
println!();
Expand Down
35 changes: 31 additions & 4 deletions src/cli/commands/prune.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
//! Prune command implementation
//!
//! Deletes local branches that have been merged into the default branch.
//! Deletes local branches that have been merged into the manifest target.
//! Optionally prunes remote tracking refs.

use crate::cli::output::Output;
use crate::core::manifest::Manifest;
use crate::core::repo::{filter_repos, RepoInfo};
use crate::git::branch::{delete_local_branch, is_branch_merged, list_local_branches};
use crate::git::{get_current_branch, open_repo, path_exists};
use crate::git::{get_current_branch, get_default_branch, open_repo, path_exists};
use crate::util::log_cmd;
use std::collections::BTreeSet;
use std::path::Path;
use std::process::Command;

Expand Down Expand Up @@ -63,11 +64,37 @@ pub fn run_prune(
}
};

// Branches that must never be pruned. Three members, not two: the branch
// checked out, the manifest target, and the remote's DEFAULT branch. The
// default is the only one that asserts "permanent" rather than "currently
// interesting" -- and until the target moved off main it was protected
// only by coincidence, because target and default were the same value.
let mut protected: BTreeSet<String> = BTreeSet::new();
protected.insert(current_branch.clone());
protected.insert(repo.target_branch().to_string());
match get_default_branch(&git_repo, &repo.sync_remote) {
Some(default_branch) => {
protected.insert(default_branch);
}
None => {
// Unknown default: protect MORE, never less. A resolution failure
// that silently shrinks this set reproduces the exact defect the
// set exists to prevent, one level up, inside its own fix -- and
// it would fail with nothing going red. Say so out loud, because
// a protected-more that is silent still reads as "resolved fine".
protected.insert("main".to_string());
protected.insert("dev".to_string());
Output::warning(&format!(
"{}: could not determine the default branch (no {}/HEAD); protecting 'main' and 'dev' by name",
repo.name, repo.sync_remote
));
}
}

let mut merged_branches: Vec<String> = Vec::new();

for branch in &branches {
// Skip current branch and default branch
if branch == &current_branch || branch == repo.target_branch() {
if protected.contains(branch) {
continue;
}

Expand Down
8 changes: 7 additions & 1 deletion src/cli/commands/push.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
//! Push command implementation

use crate::cli::outcome::CliOutcomeError;
use crate::cli::output::Output;
use crate::core::manifest::Manifest;
use crate::core::repo::{filter_repos, get_manifest_repo_info, RepoInfo};
use crate::core::repo::{
filter_repos, get_manifest_repo_info, validate_repo_filters_known, RepoInfo,
};
use crate::git::remote::{force_push_branch, push_branch};
use crate::git::{get_current_branch, open_repo, path_exists};
use git2::Repository;
Expand All @@ -27,6 +30,9 @@ pub fn run_push(
repos_filter: Option<&[String]>,
group_filter: Option<&[String]>,
) -> anyhow::Result<()> {
validate_repo_filters_known(manifest, repos_filter)
.map_err(|error| CliOutcomeError::refusal(error.to_string()))?;

if !json {
if force {
Output::header("Force pushing changes...");
Expand Down
Loading
Loading