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
21 changes: 21 additions & 0 deletions project/ticket-442/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Ticket 442: Detect tracked koru runtime files that .gitignore no longer excludes

- **ID**: ticket-442
- **Owner**: claude-code
- **Status**: IN_PROGRESS
- **Workflow state**: EDIT
- **Created**: 2026-10-07

## Goal and scope

Seven repositories (semcod/prefact, repatch, tagi, nxdo, markpact/propact, subactor/orchestrator, coru-agent/coru-dev) keep `.koru/event-store.jsonl` and `.koru/events/*` tracked although `.gitignore` lists `.koru/`; they were committed before the ignore entry, so every koru run dirties the primary checkout. koru doctor/scan only checked for the `.gitignore` line. Report ignored-but-tracked runtime files instead. User authorization: investigate the last 8 hours with monag and reflex, propose prevention, then continue (`kontynuuj`).

## Acceptance criteria

- [ ] AC-01: `check_gitignore` warns and `scan_gitignore_drift` suggests untracking when ignored koru runtime files are still tracked; clean or non-Git projects pass.
- [ ] AC-02: Tests, Ruff and governance pass.

## Tracking boundary

This directory contains the minimal reviewed intent. Optional participant prose
and raw command logs are not required delivery output.
85 changes: 85 additions & 0 deletions project/ticket-442/intent.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
{
"schema": "new-project.intent/v3",
"ticket": "ticket-442",
"summary": "Detect tracked koru runtime files that .gitignore no longer excludes",
"workstream": "application",
"classification": {
"kind": "BUG",
"priority": "P2",
"origin": "requested"
},
"allowedPaths": [
"project/ticket-442/**",
"TODO.md",
"project/TICKETS.md",
"src/koru/doctor_project_health.py",
"src/koru/scan.py",
"tests/test_doctor_tracked_runtime.py",
"tests/test_scan.py"
],
"forbiddenPaths": [
"project/ticket-*/user-*.md"
],
"stacks": [],
"dependsOn": [],
"conflictsWith": [],
"integrationTicket": null,
"delivery": {
"acceptedBaseSha": "06648f2a1bacdd4254669d522ff2b9471323b658",
"targetBranch": "main",
"outcome": "koru doctor and scan report runtime files under .koru/ and .planfile/.koru/ that are excluded by .gitignore but still tracked, so they stop dirtying checkouts unnoticed.",
"nonGoals": [
"No automatic git rm in target repositories",
"No change to koru --init gitignore entries"
],
"complexity": "S",
"estimatedMinutes": 30,
"budgets": {
"maxImplementationFiles": 5,
"maxAffectedComponents": 1,
"maxPublicInterfaceChanges": 0,
"maxRuntimeDependencies": 0
},
"architecture": {
"status": "accepted",
"decision": "Add tracked_ignored_runtime_files() (git ls-files -ci --exclude-standard) to doctor_project_health; check_gitignore and scan_gitignore_drift report its result.",
"components": [
{
"name": "project-health",
"paths": [
"src/koru/doctor_project_health.py",
"src/koru/scan.py",
"tests/test_doctor_tracked_runtime.py",
"tests/test_scan.py"
]
}
],
"responsibilityChanges": false,
"interfaceChanges": [],
"dataChanges": [],
"ui": {
"impact": "none",
"states": [],
"evidence": []
},
"rollback": "Revert the two source files and the new test."
},
"runtimeDependencies": [],
"validation": [
{
"criterion": "AC-01",
"commands": [
"python3 -m pytest -q tests/test_doctor_tracked_runtime.py tests/test_scan.py"
],
"evidence": "Tracked ignored runtime files produce WARN and a scan suggestion; untracked or non-Git projects pass."
},
{
"criterion": "AC-02",
"commands": [
"./project/governance-check.sh"
],
"evidence": "Native governance validation passes."
}
]
}
}
36 changes: 33 additions & 3 deletions src/koru/doctor_project_health.py
Original file line number Diff line number Diff line change
Expand Up @@ -373,15 +373,45 @@ def check_policy_yaml(project: Path) -> tuple[str, str]:
return PASS, "parses; loaded values match schema"


RUNTIME_PATHS = (".koru", ".planfile/.koru")


def tracked_ignored_runtime_files(project: Path) -> list[str]:
"""Return koru runtime files Git still tracks although ``.gitignore`` excludes them.

A ``.gitignore`` entry does not untrack files committed earlier, so they
keep dirtying the checkout on every koru run. Empty outside a Git checkout.
"""
try:
result = subprocess.run(
["git", "-C", str(project), "ls-files", "-ci", "--exclude-standard", "--", *RUNTIME_PATHS],
capture_output=True,
text=True,
timeout=10,
check=False,
)
except (OSError, subprocess.TimeoutExpired):
return []
if result.returncode != 0:
return []
return [line for line in result.stdout.splitlines() if line]


def check_gitignore(project: Path) -> tuple[str, str]:
gi = project / ".gitignore"
if not gi.exists():
return WARN, ".gitignore missing — runtime artefacts may be committed"
text = gi.read_text(encoding="utf-8")
needle = ".planfile/.koru/"
if any(line.strip() == needle for line in text.splitlines()):
return PASS, f"ignores {needle}"
return WARN, f".gitignore does not list {needle} — re-run `koru --init`"
if not any(line.strip() == needle for line in text.splitlines()):
return WARN, f".gitignore does not list {needle} — re-run `koru --init`"
tracked = tracked_ignored_runtime_files(project)
if tracked:
return WARN, (
f"{len(tracked)} ignored koru runtime file(s) still tracked "
f"(e.g. {tracked[0]}) — untrack with `git rm --cached`"
)
return PASS, f"ignores {needle}"


def resolve_pytest_collect_timeout() -> float:
Expand Down
19 changes: 18 additions & 1 deletion src/koru/scan.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
from pathlib import Path
from typing import Any

from koru.doctor_project_health import tracked_ignored_runtime_files
from koru.fleet_admission import scan_admission
from koru.scan_artifacts import (
_ANALYSIS_ARTIFACT_PATHS as _ANALYSIS_ARTIFACT_PATHS, # noqa: F401
Expand Down Expand Up @@ -448,7 +449,23 @@ def scan_gitignore_drift(project: Path) -> list[Suggestion]:
except OSError:
return []
if needle in text:
return []
tracked = tracked_ignored_runtime_files(project)
if not tracked:
return []
return [
Suggestion(
signal="gitignore_drift",
title="Untrack koru runtime files excluded by `.gitignore`",
description=(
f"{len(tracked)} koru runtime file(s) are ignored but still tracked, "
"so every koru run dirties the checkout. Run "
"`git rm --cached` on them in a ticket commit."
),
priority="low",
labels=("hygiene", "scan"),
files=tuple(tracked[:20]),
),
]
return [
Suggestion(
signal="gitignore_drift",
Expand Down
50 changes: 50 additions & 0 deletions tests/test_doctor_tracked_runtime.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
"""Ignored-but-tracked koru runtime files are reported (ticket-442)."""

import subprocess
from pathlib import Path

import pytest

from koru.doctor_constants import PASS, WARN
from koru.doctor_project_health import check_gitignore, tracked_ignored_runtime_files
from koru.scan import scan_gitignore_drift


def _git(project: Path, *args: str) -> None:
subprocess.run(["git", "-C", str(project), *args], check=True, capture_output=True)


@pytest.fixture
def repo(tmp_path: Path) -> Path:
_git(tmp_path, "init", "-q")
_git(tmp_path, "config", "user.email", "t@example.invalid")
_git(tmp_path, "config", "user.name", "t")
(tmp_path / ".koru").mkdir()
(tmp_path / ".koru" / "event-store.jsonl").write_text("{}\n")
(tmp_path / "README.md").write_text("x\n")
_git(tmp_path, "add", "-f", ".")
_git(tmp_path, "commit", "-qm", "seed")
(tmp_path / ".gitignore").write_text(".planfile/.koru/\n.koru/\n")
return tmp_path


def test_tracked_runtime_file_is_reported(repo: Path) -> None:
assert tracked_ignored_runtime_files(repo) == [".koru/event-store.jsonl"]
status, detail = check_gitignore(repo)
assert status == WARN
assert "git rm --cached" in detail
[suggestion] = scan_gitignore_drift(repo)
assert suggestion.files == (".koru/event-store.jsonl",)


def test_untracked_runtime_passes(repo: Path) -> None:
_git(repo, "rm", "-q", "--cached", ".koru/event-store.jsonl")
assert tracked_ignored_runtime_files(repo) == []
assert check_gitignore(repo)[0] == PASS
assert scan_gitignore_drift(repo) == []


def test_outside_git_checkout_is_empty(tmp_path: Path) -> None:
(tmp_path / ".gitignore").write_text(".planfile/.koru/\n")
assert tracked_ignored_runtime_files(tmp_path) == []
assert check_gitignore(tmp_path)[0] == PASS
Loading