diff --git a/AGENTS.md b/AGENTS.md index 88b3ee5..9bc747e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -11,6 +11,16 @@ These rules apply to all AI agents working on this repository (Codex, Claude, Co - **Never commit secrets** (.env, API keys, tokens, credentials). - **Never skip hooks** (`--no-verify`). +## Bugfix Verification + +Every bugfix **must** include a regression test that reproduces the **exact user-reported symptom**, not just a related scenario. + +1. **Reproduce first:** Before writing the fix, write a test that fails with the exact symptom the user described. "User does X, expects Y, gets Z" → the test must assert Y and currently produce Z. +2. **Fix, then re-run:** Apply the fix, confirm the test passes. +3. **Green suite is necessary, not sufficient:** All existing tests passing does not prove the reported bug is fixed. The regression test is what proves it. + +> **Why this rule exists:** v2.7.3 shipped a doctor fix that used `all_entries_unfiltered()` instead of `all_entries()`. All tests were green, but the actual bug (phantom stale tasks invisible to `palaia list`) was not fixed because no test reproduced the specific scenario (private scope + different agent). + ## Pull Requests - Keep PR titles short (<70 chars), use conventional prefix. diff --git a/CHANGELOG.md b/CHANGELOG.md index 5fc44d1..880e64f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ### Fixed - **ContextEngine: `ownsCompaction: true`** — palaia now declares compaction ownership, preventing OpenClaw's built-in Pi auto-compaction from running in parallel with `palaia gc`. Without this flag, both compaction mechanisms could interfere with each other, leading to unpredictable context truncation. -- **Doctor phantom stale-tasks warning** — `_check_stale_unassigned_tasks` now reads entries through `Store.all_entries_unfiltered()` instead of scanning `.md` files directly. The previous approach could report entries invisible to `palaia list` (e.g. entries with empty/invalid scope), causing the doctor to warn about tasks that the user cannot see or act on. +- **Doctor phantom stale-tasks warning** — `_check_stale_unassigned_tasks` now uses `Store.all_entries()` with agent scope filtering (not `all_entries_unfiltered()`). The doctor only reports entries that `palaia list` can actually see — private-scoped entries from other agents are no longer counted as phantom warnings. - **Scope: empty/unknown scope no longer hides entries** — `can_access()` now treats empty or unrecognized scope values as `"team"` instead of returning `False`. Entries with missing or malformed scope are no longer silently invisible. --- diff --git a/palaia/doctor/checks.py b/palaia/doctor/checks.py index efca47f..a4d1384 100644 --- a/palaia/doctor/checks.py +++ b/palaia/doctor/checks.py @@ -1864,9 +1864,9 @@ def _check_claude_code_config(palaia_root: Path | None) -> dict[str, Any]: def _check_stale_unassigned_tasks(palaia_root: Path | None) -> dict[str, Any]: """Check for auto-captured tasks without assignee/due_date older than 7 days. - Uses Store.all_entries_unfiltered() to ensure consistent entry discovery - with `palaia list`. Previous implementation scanned .md files directly, - which could report entries invisible to the user (scope filtering mismatch). + Uses Store.all_entries() with agent scope filtering to ensure the doctor + only reports entries visible to `palaia list`. Entries hidden by scope + (e.g. private entries from a different agent) are excluded. """ if palaia_root is None: return { @@ -1878,6 +1878,7 @@ def _check_stale_unassigned_tasks(palaia_root: Path | None) -> dict[str, Any]: from datetime import datetime, timezone + from palaia.config import resolve_agent as resolve_agent_identity from palaia.store import Store now = datetime.now(tz=timezone.utc) @@ -1885,7 +1886,8 @@ def _check_stale_unassigned_tasks(palaia_root: Path | None) -> dict[str, Any]: try: store = Store(palaia_root) - all_entries = store.all_entries_unfiltered(include_cold=False) + agent = resolve_agent_identity(palaia_root) + all_entries = store.all_entries(include_cold=False, agent=agent) except Exception: return { "name": "stale_unassigned_tasks", diff --git a/tests/test_doctor.py b/tests/test_doctor.py index db49d14..ecaedb1 100644 --- a/tests/test_doctor.py +++ b/tests/test_doctor.py @@ -438,6 +438,36 @@ def test_ignores_tasks_with_assignee(self, palaia_root): result = _check_stale_unassigned_tasks(palaia_root) assert result["status"] == "ok" + def test_ignores_private_tasks_from_other_agent(self, palaia_root, monkeypatch): + """Regression: doctor must not report private-scoped entries from a + different agent. This was the exact user-reported bug — doctor showed + 4 stale tasks that ``palaia list`` didn't because scope filtering was + bypassed (all_entries_unfiltered instead of all_entries). + """ + from datetime import datetime, timedelta, timezone + + from palaia.doctor import _check_stale_unassigned_tasks + + old_date = (datetime.now(tz=timezone.utc) - timedelta(days=10)).isoformat() + entry_content = f"""--- +id: private-other-agent-001 +type: task +tags: auto-capture,commitment +created: {old_date} +scope: private +agent: agent-a +title: Task only visible to agent-a +--- +This task belongs to agent-a and must be invisible to agent-b.""" + (palaia_root / "hot" / "private-other-agent-001.md").write_text(entry_content) + + # Doctor runs as agent-b → private entry from agent-a must be invisible + monkeypatch.setenv("PALAIA_AGENT", "agent-b") + result = _check_stale_unassigned_tasks(palaia_root) + assert result["status"] == "ok", ( + f"Doctor reported private entry from agent-a while running as agent-b: {result}" + ) + def test_ignores_manual_tasks(self, palaia_root): from datetime import datetime, timedelta, timezone