diff --git a/project/TICKETS.md b/project/TICKETS.md index ae48b21e..f9320f1e 100644 --- a/project/TICKETS.md +++ b/project/TICKETS.md @@ -441,4 +441,5 @@ project analysis or human-owned ticket inputs. | **ticket-441** | [`README.md`](./ticket-441/README.md) | - | - | - | - | - | | **ticket-442** | [`README.md`](./ticket-442/README.md) | - | - | - | - | - | | **ticket-443** | [`README.md`](./ticket-443/README.md) | - | - | - | - | - | +| **ticket-445** | [`README.md`](./ticket-445/README.md) | - | - | - | - | - | diff --git a/project/ticket-447/README.md b/project/ticket-447/README.md new file mode 100644 index 00000000..57606a70 --- /dev/null +++ b/project/ticket-447/README.md @@ -0,0 +1,19 @@ +# Ticket 447: Support force staging and add returncode check for verified patch paths in commit_worktree + +- **ID**: ticket-447 +- **Owner**: gemini +- **Status**: IN_PROGRESS +- **Workflow state**: PUBLICATION +- **Created**: 2026-10-11 + +SESSION_EXECUTION_AUTHORIZATION: user requests continuation, test and merge. Koru verified patch staging must handle paths matched by broad repository gitignore and check staging exit codes before committing. + +## Goal and scope + +In `src/koru/queue/workspace.py`, when committing verified patch paths on a run branch or main, use force staging (`git add -f -- `) for explicitly passed patch paths and check command returncode so that files ignored by broad target `.gitignore` rules (e.g. `.planfile/`) can be committed properly to the run branch, and failures in `git add` are cleanly reported rather than failing ambiguously during `git commit`. + +## Acceptance criteria + +- [x] AC-01: `commit_worktree` uses `git add -f -- ` when explicit paths are supplied and checks returncode before invoking `git commit`. +- [x] AC-02: `commit_on_main` uses `git add -f -- ` when explicit paths are supplied. +- [x] AC-03: Regression tests verify that verified patch paths ignored by `.gitignore` are staged and committed successfully to the run branch. diff --git a/project/ticket-447/intent.json b/project/ticket-447/intent.json new file mode 100644 index 00000000..38c70490 --- /dev/null +++ b/project/ticket-447/intent.json @@ -0,0 +1,75 @@ +{ + "schema": "new-project.intent/v3", + "ticket": "ticket-447", + "summary": "Support force staging and add returncode check for verified patch paths in commit_worktree", + "workstream": "application", + "classification": { + "kind": "BUG", + "priority": "P2", + "origin": "health" + }, + "allowedPaths": [ + "project/ticket-447/**", + "TODO.md", + "project/TICKETS.md", + "src/koru/queue/workspace.py", + "tests/test_queue_workspace_retention.py" + ], + "forbiddenPaths": [ + "project/ticket-*/user-*.md" + ], + "stacks": [], + "dependsOn": [], + "conflictsWith": [], + "integrationTicket": null, + "delivery": { + "acceptedBaseSha": "e960b60e6316e557fb7a0034b9fef48317d2de09", + "targetBranch": "main", + "outcome": "Ensure explicit verified patch paths are staged with -f in commit_worktree and commit_on_main, and verify staging exit code before commit.", + "nonGoals": [ + "No changes to admission checks or governance rules", + "No changes to promotion modes or verification pipeline" + ], + "complexity": "S", + "estimatedMinutes": 30, + "budgets": { + "maxImplementationFiles": 2, + "maxAffectedComponents": 1, + "maxPublicInterfaceChanges": 0, + "maxRuntimeDependencies": 0 + }, + "architecture": { + "status": "accepted", + "decision": "Use -f flag when staging explicit verified patch paths in commit_worktree and commit_on_main, and verify command exit code before commit.", + "components": [ + { + "name": "queue-workspace", + "paths": [ + "src/koru/queue/workspace.py", + "tests/test_queue_workspace_retention.py" + ] + } + ], + "responsibilityChanges": false, + "interfaceChanges": [], + "dataChanges": [], + "ui": { + "impact": "none", + "states": [], + "evidence": [] + }, + "rollback": "Revert the commit on ticket-447." + }, + "runtimeDependencies": [], + "validation": [ + { + "criterion": "AC-01", + "commands": [ + "pytest tests/test_queue_workspace_retention.py", + "./project/governance-check.sh" + ], + "evidence": "Regression test verifying that verified patch paths matched by .gitignore rules are staged and committed properly." + } + ] + } +} diff --git a/src/koru/queue/workspace.py b/src/koru/queue/workspace.py index e06e1b88..491f693b 100644 --- a/src/koru/queue/workspace.py +++ b/src/koru/queue/workspace.py @@ -200,7 +200,7 @@ def commit_on_main( ) -> tuple[bool, str]: """Commit only the patch's files on the current branch.""" if paths: - staged = _git(project, "add", "--", *paths) + staged = _git(project, "add", "-f", "--", *paths) else: staged = _git(project, "add", "-A") if staged.returncode != 0: @@ -227,7 +227,12 @@ def commit_worktree( switched = _git(worktree, "switch", "--quiet", "-c", branch) if switched.returncode != 0: return False, (switched.stderr or "").strip()[:300] - _git(worktree, "add", "--", *paths) if paths else _git(worktree, "add", "-A") + if paths: + staged = _git(worktree, "add", "-f", "--", *paths) + else: + staged = _git(worktree, "add", "-A") + if staged.returncode != 0: + return False, (staged.stderr or staged.stdout or "").strip()[:300] committed = _git(worktree, "commit", "--quiet", "-m", message) if committed.returncode != 0: return False, (committed.stderr or committed.stdout or "").strip()[:300] diff --git a/tests/test_queue_workspace_retention.py b/tests/test_queue_workspace_retention.py index cd8427ff..e5933b13 100644 --- a/tests/test_queue_workspace_retention.py +++ b/tests/test_queue_workspace_retention.py @@ -126,7 +126,9 @@ def concurrent_removal(project: Path, *args: str, **kwargs): @pytest.mark.parametrize("linked", [False, True]) def test_governed_staging_requires_controller(repo: Path, linked: bool) -> None: - (repo / ".governance").mkdir() + gov = repo / ".governance" + gov.mkdir() + (gov / "manifest.json").write_text("{}") root = repo if linked: root = repo.parent / "existing-linked" @@ -145,7 +147,9 @@ def test_governed_staging_requires_controller(repo: Path, linked: bool) -> None: @pytest.mark.parametrize("mode", ["branch", "apply"]) def test_governed_refusal_does_not_run_verifier_or_apply(repo: Path, mode: str) -> None: - (repo / ".governance").mkdir() + gov = repo / ".governance" + gov.mkdir() + (gov / "manifest.json").write_text("{}") diff = "diff --git a/a.txt b/a.txt\n--- a/a.txt\n+++ b/a.txt\n@@ -1 +1 @@\n-old\n+new\n" plan = build_patch_plan(repo, {"id": "T-1", "inputs": {"promotion_mode": mode}}, diff) runner = Mock(side_effect=AssertionError("verifier must not run")) @@ -183,3 +187,21 @@ def test_git_override_is_refused_before_any_git_effect(repo: Path, monkeypatch: with pytest.raises(StagingAdmissionRequired, match="overrides"): with staging_worktree(repo, ()): pytest.fail("redirected workspace admitted") + + +def test_commit_worktree_stages_explicit_paths_even_if_ignored(repo: Path) -> None: + _repolab.commit_file(repo, ".gitignore", ".planfile/\n") + with staging_worktree(repo, ()) as staged: + assert staged is not None + planfile_file = staged / ".planfile" / "sync" / "state.yaml" + planfile_file.parent.mkdir(parents=True) + planfile_file.write_text("version: 1\n") + ok, detail = workspace.commit_worktree( + staged, + "koru/run-test-ignored", + "koru(T-1): commit test", + (".planfile/sync/state.yaml",), + ) + assert ok, f"commit_worktree failed: {detail}" + committed_content = git(staged, "show", "koru/run-test-ignored:.planfile/sync/state.yaml") + assert committed_content.strip() == "version: 1"