From 00a6af363606a6d9a591f5c3bcf914e86f0fde4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=CE=BB=F0=9F=8F=BA?= <140930+codekiln@users.noreply.github.com> Date: Tue, 20 Jan 2026 08:01:41 -0500 Subject: [PATCH 1/4] =?UTF-8?q?=E2=9C=A8=20feat(prompt):=20add=20parent=20?= =?UTF-8?q?commit=20options=20to=20resolve=20409=20conflicts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds three new CLI options to `langstar prompt push`: - --parent-commit : Specify parent commit explicitly - --auto-parent: Automatically fetch latest commit as parent - --force: Push without parent validation (with warnings) This resolves 409 conflicts when updating existing prompts by providing the parent commit reference that the LangSmith API requires. SDK Changes: - Add CommitManifestResponse type for full commit metadata - Add get_commit() method to fetch commit hash and manifest - Refactor pull() to use get_commit() internally CLI Changes: - Add new flags with conflicts_with constraint - Implement auto-fetch logic for --auto-parent - Add warnings for --force flag - Pass parent_commit to SDK for both regular and structured prompts Tests: - Update mock responses to include commit_hash field - All 609 tests passing Fixes #719 Co-Authored-By: Claude --- cli/src/commands/prompt.rs | 61 ++++++++++++++++++++++++-- sdk/src/lib.rs | 5 ++- sdk/src/prompts.rs | 65 +++++++++++++++++++++++----- sdk/tests/structured_prompts_test.rs | 2 + 4 files changed, 117 insertions(+), 16 deletions(-) diff --git a/cli/src/commands/prompt.rs b/cli/src/commands/prompt.rs index 435cd41f..f5c408d7 100644 --- a/cli/src/commands/prompt.rs +++ b/cli/src/commands/prompt.rs @@ -126,6 +126,18 @@ pub enum PromptCommands { #[arg(long, default_value = "json_schema")] schema_method: String, + /// Parent commit hash for updates (resolves 409 conflicts) + #[arg(long, value_name = "HASH", conflicts_with = "auto_parent")] + parent_commit: Option, + + /// Automatically fetch and use latest commit as parent + #[arg(long, conflicts_with = "parent_commit")] + auto_parent: bool, + + /// Force push without parent commit validation (use with caution) + #[arg(long)] + force: bool, + /// Organization ID for scoping (overrides config/env) #[arg(long)] organization_id: Option, @@ -588,6 +600,9 @@ impl PromptCommands { template_format, schema, schema_method, + parent_commit, + auto_parent, + force, organization_id, workspace_id, } => { @@ -619,9 +634,10 @@ impl PromptCommands { let repo_handle = format!("{}/{}", owner, repo); formatter.info(&format!("Checking if repository {} exists...", repo_handle)); - match client.prompts().get(&repo_handle).await { + let repo_exists = match client.prompts().get(&repo_handle).await { Ok(_) => { println!("✓ Repository exists"); + true } Err(_) => { formatter.info(&format!( @@ -641,14 +657,46 @@ impl PromptCommands { { Ok(_) => { println!("✓ Repository created successfully"); + false // New repo, no commits yet } Err(e) => { eprintln!("⚠ Warning: Could not create repository: {}", e); eprintln!(" Will attempt to push anyway..."); + true // Assume it exists and proceed } } } - } + }; + + // Determine parent commit for the push + let final_parent_commit = if *force { + eprintln!("⚠ Warning: Using --force flag"); + eprintln!(" This will push without parent commit validation"); + eprintln!(" May overwrite concurrent changes to the prompt"); + None + } else if *auto_parent { + if repo_exists { + formatter.info("Fetching latest commit as parent..."); + match client.prompts().get_commit(owner, repo, "latest").await { + Ok(commit_info) => { + println!("✓ Latest commit: {}", commit_info.commit_hash); + Some(commit_info.commit_hash) + } + Err(e) => { + eprintln!("⚠ Warning: Could not fetch latest commit: {}", e); + eprintln!( + " Proceeding without parent commit (assuming first commit)" + ); + None + } + } + } else { + formatter.info("New repository, no parent commit needed"); + None + } + } else { + parent_commit.clone() + }; // Parse input variables let vars: Vec = if let Some(vars_str) = input_variables { @@ -727,7 +775,12 @@ impl PromptCommands { // Push structured prompt client .prompts() - .push_structured_prompt(owner, repo, structured_prompt, None) + .push_structured_prompt( + owner, + repo, + structured_prompt, + final_parent_commit.clone(), + ) .await .map_err(crate::error::CliError::Sdk)? } else { @@ -741,7 +794,7 @@ impl PromptCommands { "input_variables": vars, "template_format": template_format }), - parent_commit: None, + parent_commit: final_parent_commit, example_run_ids: None, }; diff --git a/sdk/src/lib.rs b/sdk/src/lib.rs index da1052eb..c15ceb6b 100644 --- a/sdk/src/lib.rs +++ b/sdk/src/lib.rs @@ -95,8 +95,9 @@ pub use projects::{ ListProjectsParams, Project, ProjectCreate, ProjectSortColumn, ProjectUpdate, TraceTier, }; pub use prompts::{ - CommitRequest, CommitResponse, LcJson, MessagePromptTemplateKwargs, Prompt, PromptClient, - PromptTemplateKwargs, StructuredOutputKwargs, StructuredPrompt, Visibility, + CommitManifestResponse, CommitRequest, CommitResponse, LcJson, MessagePromptTemplateKwargs, + Prompt, PromptClient, PromptTemplateKwargs, StructuredOutputKwargs, StructuredPrompt, + Visibility, }; pub use runs::{Cursors, QueryRunsRequest, QueryRunsResponse, Run, RunDateOrder, RunType}; pub use secrets::{SecretKey, SecretUpsert}; diff --git a/sdk/src/prompts.rs b/sdk/src/prompts.rs index 83c1e458..f2f5055f 100644 --- a/sdk/src/prompts.rs +++ b/sdk/src/prompts.rs @@ -588,6 +588,44 @@ impl<'a> PromptClient<'a> { self.push(owner, repo, &commit_request).await } + /// Get commit metadata including hash and manifest. + /// + /// This retrieves the full commit response including the commit hash, + /// which is needed when creating child commits with parent references. + /// + /// # Arguments + /// * `owner` - The owner of the prompt (username or organization) + /// * `repo` - The prompt repository name + /// * `commit` - The commit hash or tag (e.g., "latest", "main", commit SHA) + /// + /// # Returns + /// The full commit response including commit_hash and manifest + /// + /// # Example + /// ```no_run + /// # use langstar_sdk::{AuthConfig, LangchainClient}; + /// # async fn example() -> langstar_sdk::Result<()> { + /// let auth = AuthConfig::from_env()?; + /// let client = LangchainClient::new(auth)?; + /// let prompts = client.prompts(); + /// + /// let commit_info = prompts.get_commit("owner", "my-prompt", "latest").await?; + /// println!("Latest commit hash: {}", commit_info.commit_hash); + /// # Ok(()) + /// # } + /// ``` + pub async fn get_commit( + &self, + owner: &str, + repo: &str, + commit: &str, + ) -> Result { + let path = format!("/api/v1/commits/{}/{}/{}", owner, repo, commit); + let request = self.client.langsmith_get(&path)?; + let response: CommitManifestResponse = self.client.execute(request).await?; + Ok(response) + } + /// Pull a prompt commit from the PromptHub. /// /// This retrieves a specific commit from the prompt repository. @@ -613,16 +651,8 @@ impl<'a> PromptClient<'a> { /// # } /// ``` pub async fn pull(&self, owner: &str, repo: &str, commit: &str) -> Result { - let path = format!("/api/v1/commits/{}/{}/{}", owner, repo, commit); - let request = self.client.langsmith_get(&path)?; - - #[derive(Deserialize)] - struct CommitManifestResponse { - manifest: Value, - } - - let response: CommitManifestResponse = self.client.execute(request).await?; - Ok(response.manifest) + let commit_response = self.get_commit(owner, repo, commit).await?; + Ok(commit_response.manifest) } /// Pull a structured prompt from the PromptHub and deserialize it. @@ -768,6 +798,21 @@ pub struct CommitData { pub url: Option, } +/// Response from getting commit metadata +/// +/// Returned by the GET /api/v1/commits/{owner}/{repo}/{commit} endpoint. +/// Includes both the commit hash and the full manifest. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CommitManifestResponse { + /// The commit hash + pub commit_hash: String, + /// The commit manifest (prompt template/schema) + pub manifest: serde_json::Value, + /// Optional example run IDs + #[serde(skip_serializing_if = "Option::is_none")] + pub examples: Option>, +} + /// Data for creating/updating a prompt (deprecated, use CommitRequest) /// /// This type is kept for backward compatibility but CommitRequest should be diff --git a/sdk/tests/structured_prompts_test.rs b/sdk/tests/structured_prompts_test.rs index 5c0b6f34..54182688 100644 --- a/sdk/tests/structured_prompts_test.rs +++ b/sdk/tests/structured_prompts_test.rs @@ -203,6 +203,7 @@ async fn test_pull_structured_prompt_success() { }); let mock_response = json!({ + "commit_hash": "abc123def456", "manifest": { "lc": 1, "type": "constructor", @@ -343,6 +344,7 @@ async fn test_structured_prompt_round_trip_mock() { // Step 2: Mock pull (simulate API returning what we just pushed) let wrapped_prompt = original_prompt.to_lc_json(); let pull_response = json!({ + "commit_hash": commit_hash.clone(), "manifest": serde_json::to_value(&wrapped_prompt).unwrap() }); From 474b70d919270e42b325e5e7ff08b0bf494e9a84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=CE=BB=F0=9F=8F=BA?= <140930+codekiln@users.noreply.github.com> Date: Thu, 22 Jan 2026 07:55:36 -0500 Subject: [PATCH 2/4] =?UTF-8?q?=F0=9F=A7=AA=20test(prompt):=20address=20Co?= =?UTF-8?q?pilot=20review=20comments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add dedicated test coverage for get_commit() method - Make --force flag mutually exclusive with --parent-commit and --auto-parent - Fix repo_exists logic when repository creation fails All three changes improve code quality and prevent ambiguous behavior. Tests: All 610 tests passing. Co-Authored-By: Claude --- cli/src/commands/prompt.rs | 8 ++-- sdk/tests/structured_prompts_test.rs | 59 ++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 3 deletions(-) diff --git a/cli/src/commands/prompt.rs b/cli/src/commands/prompt.rs index f5c408d7..7dd91786 100644 --- a/cli/src/commands/prompt.rs +++ b/cli/src/commands/prompt.rs @@ -135,7 +135,7 @@ pub enum PromptCommands { auto_parent: bool, /// Force push without parent commit validation (use with caution) - #[arg(long)] + #[arg(long, conflicts_with_all = ["parent_commit", "auto_parent"])] force: bool, /// Organization ID for scoping (overrides config/env) @@ -661,8 +661,10 @@ impl PromptCommands { } Err(e) => { eprintln!("⚠ Warning: Could not create repository: {}", e); - eprintln!(" Will attempt to push anyway..."); - true // Assume it exists and proceed + eprintln!( + " Will attempt to push anyway, but auto-parent will be disabled..." + ); + false // Repository creation failed; treat as non-existent for auto-parent } } } diff --git a/sdk/tests/structured_prompts_test.rs b/sdk/tests/structured_prompts_test.rs index 54182688..1460604a 100644 --- a/sdk/tests/structured_prompts_test.rs +++ b/sdk/tests/structured_prompts_test.rs @@ -270,6 +270,65 @@ async fn test_pull_structured_prompt_success() { ); } +#[tokio::test] +async fn test_get_commit_success() { + let mut server = Server::new_async().await; + + // Create a mock response with commit_hash and manifest + let schema = json!({ + "type": "object", + "properties": { + "answer": {"type": "string"} + }, + "required": ["answer"] + }); + + let mock_response = json!({ + "commit_hash": "test-commit-hash-123", + "manifest": { + "lc": 1, + "type": "constructor", + "id": ["langchain_core", "prompts", "structured", "StructuredPrompt"], + "kwargs": { + "input_variables": ["question"], + "messages": [], + "schema_": schema, + "structured_output_kwargs": { + "method": "json_schema" + } + } + } + }); + + let mock = server + .mock("GET", "/api/v1/commits/test-owner/test-repo/abc123") + .match_header("x-api-key", "test-api-key") + .match_header("x-organization-id", "test-org-id") + .with_status(200) + .with_header("Content-Type", "application/json") + .with_body(mock_response.to_string()) + .create_async() + .await; + + let client = create_test_client(&server); + + let result = client + .prompts() + .get_commit("test-owner", "test-repo", "abc123") + .await; + + mock.assert_async().await; + assert!(result.is_ok()); + + let commit_response = result.unwrap(); + assert_eq!(commit_response.commit_hash, "test-commit-hash-123"); + assert!(commit_response.manifest.is_object()); + assert_eq!( + commit_response.manifest["kwargs"]["structured_output_kwargs"]["method"], + "json_schema" + ); +} + #[tokio::test] async fn test_pull_structured_prompt_deserialization_error() { let mut server = Server::new_async().await; From 7877a9c8a346919a071ae64a83c99196e44f2382 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=CE=BB=F0=9F=8F=BA?= <140930+codekiln@users.noreply.github.com> Date: Fri, 23 Jan 2026 08:50:56 -0500 Subject: [PATCH 3/4] =?UTF-8?q?=E2=9C=A8=20feat(prompt):=20make=20auto-par?= =?UTF-8?q?ent=20default=20behavior=20for=20push=20updates?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolves 409 conflicts by automatically fetching latest commit as parent when updating existing prompts, following Git-like UX expectations. Changes: - Remove --parent-commit, --auto-parent, and --force flags - Make parent commit fetching automatic and transparent - New repos: no parent needed (first commit) - Existing repos: auto-fetch latest commit as parent - Graceful error handling with fallback to no parent Fixes #719 Co-Authored-By: Claude --- cli/src/commands/prompt.rs | 53 ++++++++++---------------------------- 1 file changed, 14 insertions(+), 39 deletions(-) diff --git a/cli/src/commands/prompt.rs b/cli/src/commands/prompt.rs index 7dd91786..93bc0464 100644 --- a/cli/src/commands/prompt.rs +++ b/cli/src/commands/prompt.rs @@ -126,18 +126,6 @@ pub enum PromptCommands { #[arg(long, default_value = "json_schema")] schema_method: String, - /// Parent commit hash for updates (resolves 409 conflicts) - #[arg(long, value_name = "HASH", conflicts_with = "auto_parent")] - parent_commit: Option, - - /// Automatically fetch and use latest commit as parent - #[arg(long, conflicts_with = "parent_commit")] - auto_parent: bool, - - /// Force push without parent commit validation (use with caution) - #[arg(long, conflicts_with_all = ["parent_commit", "auto_parent"])] - force: bool, - /// Organization ID for scoping (overrides config/env) #[arg(long)] organization_id: Option, @@ -600,9 +588,6 @@ impl PromptCommands { template_format, schema, schema_method, - parent_commit, - auto_parent, - force, organization_id, workspace_id, } => { @@ -671,33 +656,23 @@ impl PromptCommands { }; // Determine parent commit for the push - let final_parent_commit = if *force { - eprintln!("⚠ Warning: Using --force flag"); - eprintln!(" This will push without parent commit validation"); - eprintln!(" May overwrite concurrent changes to the prompt"); - None - } else if *auto_parent { - if repo_exists { - formatter.info("Fetching latest commit as parent..."); - match client.prompts().get_commit(owner, repo, "latest").await { - Ok(commit_info) => { - println!("✓ Latest commit: {}", commit_info.commit_hash); - Some(commit_info.commit_hash) - } - Err(e) => { - eprintln!("⚠ Warning: Could not fetch latest commit: {}", e); - eprintln!( - " Proceeding without parent commit (assuming first commit)" - ); - None - } + // Automatically fetch latest commit as parent if repo exists + let final_parent_commit = if repo_exists { + formatter.info("Fetching latest commit as parent..."); + match client.prompts().get_commit(owner, repo, "latest").await { + Ok(commit_info) => { + println!("✓ Latest commit: {}", commit_info.commit_hash); + Some(commit_info.commit_hash) + } + Err(e) => { + eprintln!("⚠ Warning: Could not fetch latest commit: {}", e); + eprintln!(" Proceeding without parent commit (assuming first commit)"); + None } - } else { - formatter.info("New repository, no parent commit needed"); - None } } else { - parent_commit.clone() + formatter.info("New repository, no parent commit needed"); + None }; // Parse input variables From e33cd3ea4516acde73ec59d05316c9f2e9d25cc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=CE=BB=F0=9F=8F=BA?= <140930+codekiln@users.noreply.github.com> Date: Fri, 23 Jan 2026 08:51:06 -0500 Subject: [PATCH 4/4] =?UTF-8?q?=F0=9F=93=9A=20docs:=20add=20implementation?= =?UTF-8?q?=20plan=20for=20auto-parent=20default=20behavior?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../719-prompt-push-auto-parent-default.md | 262 ++++++++++++++++++ 1 file changed, 262 insertions(+) create mode 100644 docs/implementation/719-prompt-push-auto-parent-default.md diff --git a/docs/implementation/719-prompt-push-auto-parent-default.md b/docs/implementation/719-prompt-push-auto-parent-default.md new file mode 100644 index 00000000..7e494cfe --- /dev/null +++ b/docs/implementation/719-prompt-push-auto-parent-default.md @@ -0,0 +1,262 @@ +# Plan: Fix Issue #719 - Auto-Parent Commit as Default Behavior + +**Issue**: #719 - "prompt push fails with 409 conflict when updating existing prompt" +**Milestone**: #16 (ls-prompt-ux) +**Date**: 2026-01-23 + +## Context + +**Root Cause**: LangSmith API requires parent commit when updating existing prompts +**Current State**: PR #723 added three flags (`--parent-commit`, `--auto-parent`, `--force`) but this is over-engineered + +**Key Insight from User**: +> "It's my expectation that 'The push should automatically fetch the latest commit hash and use it as the parent commit' as per the description of #719. That's what happens in git and it's what should happen here." + +**Design Document Context**: +The `docs/implementation/ls-prompt-ux-command-redesign.md` outlines a future CRUD redesign (Phase 2, Issue #668.2) where: +- `push` will be split into `create` (new prompts) and `update` (existing prompts) +- This is part of a broader AI-first UX redesign +- Phase 2 is planned but not yet implemented + +## Current State Analysis + +### What Exists Now +- **PR #723 implementation**: Three flags for parent commit handling +- **Uncommitted changes**: Removed all three flags, made auto-parent the default +- **Code location**: `cli/src/commands/prompt.rs` lines 100-800 +- **SDK support**: `get_commit()` method exists and works (added in PR #723) + +### Current Behavior After Changes +```rust +// If repo exists → auto-fetch latest commit as parent +// If new repo → no parent needed +let final_parent_commit = if repo_exists { + // Fetch latest commit automatically + match client.prompts().get_commit(owner, repo, "latest").await { + Ok(commit_info) => Some(commit_info.commit_hash), + Err(e) => None // Graceful fallback + } +} else { + None // New repo, no parent needed +}; +``` + +## Decision Point: Minimal Fix vs CRUD Redesign Now + +### Option A: Minimal Fix (RECOMMENDED) +Keep the current `push` command, make auto-parent the default. + +**Pros:** +- Solves #719 immediately and completely +- Simple, Git-like UX (just works™) +- No breaking changes +- Aligns with future CRUD redesign (can be implemented later in Phase 2) +- Minimal code changes (flags removed, auto-fetch as default) + +**Cons:** +- `push` still does both create AND update (slightly confusing) +- Doesn't implement the CRUD redesign yet + +### Option B: Implement CRUD Commands Now +Split `push` into `create` and `update` commands as outlined in redesign doc. + +**Pros:** +- Implements Phase 2 of redesign immediately +- Clear separation: `create` for new prompts, `update` for existing +- More explicit intent + +**Cons:** +- Much larger scope than issue #719 +- Requires additional planning and testing +- Breaking change (deprecating `push`) +- Out of scope for current milestone + +## Recommended Approach: Option A (Minimal Fix) + +**Rationale:** +1. Issue #719 is about fixing 409 conflicts, not redesigning commands +2. The CRUD redesign is planned for Phase 2 (Issue #668.2) - separate work +3. Auto-parent as default solves the problem completely and elegantly +4. Keeps changes focused and testable +5. Doesn't block future CRUD implementation + +## Implementation Plan + +### Files to Modify + +1. **`cli/src/commands/prompt.rs`** (primary changes) + - Lines 100-148: Remove three flag definitions + - Lines 588-593: Remove flag variables from pattern match + - Lines 656-678: Simplify parent commit logic (already done in uncommitted changes) + +2. **Tests to Update** + - Any CLI tests that use `--parent-commit`, `--auto-parent`, or `--force` flags + - Verify auto-parent behavior with integration tests + +3. **Documentation to Update** + - PR #723 description needs updating + - CHANGELOG.md entry should reflect "auto-parent as default" not "added flags" + +### Detailed Changes + +#### 1. Remove Flag Definitions (DONE in uncommitted changes) +```rust +// REMOVE these lines from Push struct: +/// Parent commit hash for updates (resolves 409 conflicts) +#[arg(long, value_name = "HASH", conflicts_with = "auto_parent")] +parent_commit: Option, + +/// Automatically fetch and use latest commit as parent +#[arg(long, conflicts_with = "parent_commit")] +auto_parent: bool, + +/// Force push without parent commit validation (use with caution) +#[arg(long, conflicts_with_all = ["parent_commit", "auto_parent"])] +force: bool, +``` + +#### 2. Simplify Parent Commit Logic (DONE in uncommitted changes) +```rust +// Replace complex if/else with simple auto-fetch logic +let final_parent_commit = if repo_exists { + formatter.info("Fetching latest commit as parent..."); + match client.prompts().get_commit(owner, repo, "latest").await { + Ok(commit_info) => { + println!("✓ Latest commit: {}", commit_info.commit_hash); + Some(commit_info.commit_hash) + } + Err(e) => { + eprintln!("⚠ Warning: Could not fetch latest commit: {}", e); + eprintln!(" Proceeding without parent commit (assuming first commit)"); + None + } + } +} else { + formatter.info("New repository, no parent commit needed"); + None +}; +``` + +#### 3. Update Tests +Search for any tests using the removed flags and update them: +```bash +# Find tests that might use the flags +grep -r "parent-commit\|auto-parent\|force" sdk/tests/ cli/tests/ +``` + +### Behavior Specification + +**For new prompts:** +```bash +$ langstar prompt push -o owner -r new-prompt -t "template" +ℹ Checking if repository owner/new-prompt exists... +ℹ Repository not found, creating owner/new-prompt... +✓ Repository created successfully +ℹ New repository, no parent commit needed +ℹ Pushing prompt to owner/new-prompt... +✓ Pushed successfully +``` + +**For existing prompts:** +```bash +$ langstar prompt push -o owner -r existing-prompt -t "updated template" +ℹ Checking if repository owner/existing-prompt exists... +✓ Repository exists +ℹ Fetching latest commit as parent... +✓ Latest commit: abc123def456 +ℹ Pushing prompt to owner/existing-prompt... +✓ Pushed successfully (commit: def789ghi012) +``` + +**Error handling (graceful fallback):** +```bash +$ langstar prompt push -o owner -r existing-prompt -t "template" +ℹ Checking if repository owner/existing-prompt exists... +✓ Repository exists +ℹ Fetching latest commit as parent... +⚠ Warning: Could not fetch latest commit: API error: 404 + Proceeding without parent commit (assuming first commit) +ℹ Pushing prompt to owner/existing-prompt... +✓ Pushed successfully +``` + +## Verification Plan + +### 1. Manual Testing +```bash +# Test 1: Create new prompt (should work without parent) +langstar prompt push -o "-" -r test-new-prompt -t "Hello {name}" + +# Test 2: Update existing prompt (should auto-fetch parent) +langstar prompt push -o "-" -r test-new-prompt -t "Hi {name}, how are you?" + +# Test 3: Verify no 409 errors on update +langstar prompt push -o "-" -r test-new-prompt -t "Greetings {name}" +``` + +### 2. Automated Tests +```bash +# Run full test suite +cargo nextest run --profile ci --all-features --workspace + +# Focus on prompt tests +cargo nextest run --profile ci test_push test_get_commit +``` + +### 3. Validation Criteria +- ✅ No 409 errors when updating existing prompts +- ✅ New prompts created without parent commit +- ✅ Existing prompts updated with auto-fetched parent +- ✅ Graceful fallback if parent fetch fails +- ✅ All existing tests pass +- ✅ Help text updated (no mention of removed flags) + +## Commit Strategy + +### Commit Message +``` +✨ feat(prompt): make auto-parent default behavior for push updates + +Resolves 409 conflicts by automatically fetching latest commit as parent +when updating existing prompts, following Git-like UX expectations. + +Changes: +- Remove --parent-commit, --auto-parent, and --force flags +- Make parent commit fetching automatic and transparent +- New repos: no parent needed (first commit) +- Existing repos: auto-fetch latest commit as parent +- Graceful error handling with fallback to no parent + +Fixes #719 + +Co-Authored-By: Claude +``` + +### PR Updates +1. Update PR #723 description to reflect simplified approach +2. Remove validation analysis about flags (no longer applicable) +3. Update test plan to focus on auto-parent behavior +4. Emphasize Git-like UX and simplicity + +## Future Work (Out of Scope) + +The following are planned but NOT part of this fix: + +1. **Phase 2 CRUD Redesign** (Issue #668.2): + - Split `push` into `create` and `update` commands + - Implement progressive disclosure help system + - Add `get` command with modifiers + +2. **Advanced Features** (if needed): + - `--force` flag for emergency overrides (only if use case emerges) + - `--parent-commit` for advanced scenarios (only if use case emerges) + +## Summary + +**Approach**: Minimal fix - make auto-parent the default behavior +**Scope**: Issue #719 only (409 conflict resolution) +**Changes**: Remove flags, simplify logic (already done in uncommitted changes) +**Testing**: Manual + automated verification +**Outcome**: Git-like UX where "push just works" for both create and update + +This plan solves #719 completely while staying focused and not over-engineering the solution.