diff --git a/README.md b/README.md index 8a7bdbcf..d2110823 100644 --- a/README.md +++ b/README.md @@ -211,8 +211,7 @@ is the process environment alone. | `CATSTACK_REFLECT_ENFORCEMENT=1` | env and files | the reflect hooks and rule above | | `CATSTACK_CAT_MODE_DEFAULT=1` | env and files | `cat-mode-default` applies `cat-mode` to every prompt and every subagent prompt | | `CAT_MODE_AUTO_INVOKE=true` | env, then this checkout's `.env`, when `./install.sh` runs | installs `cat-mode` so the model may invoke it without `/cat-mode` | -| `CATSTACK_HOOK_FRESHNESS=0` | env only | silences the `hook-freshness` advisory | -| `CATSTACK_HOOK_FRESHNESS_FETCH=1` | env only | lets `hook-freshness` run a short `git fetch` before counting | +| `CATSTACK_HOOK_FRESHNESS=off\|local\|fetch` | env only | `hook-freshness` mode: `off` (or `0`) silences it; `local`, the default, counts against the last-fetched `origin/main`; `fetch` runs a short `git fetch` first | | `CATSTACK_SKILL_USAGE_LOG=1` | env only | `skill-usage-log` records each Skill tool call | | `CATSTACK_LLM_JUDGE_RUNNERS` | env only | a JSON list of `[name, argv]` pairs that replaces the background judge's model runners | | `CATSTACK_DORA_GIT_ROOTS`, `CATSTACK_DORA_GH_REPOS`, `CATSTACK_DORA_DEPLOY_GIT_ONLY` | env only | session-mine DORA inputs: colon-separated git roots, comma-separated `owner/name` repos, and `1` to skip GitHub search and take merged PRs from local git only | diff --git a/corpus/CLAUDE.learned.md b/corpus/CLAUDE.learned.md index c28c34f1..69069ada 100644 --- a/corpus/CLAUDE.learned.md +++ b/corpus/CLAUDE.learned.md @@ -25,7 +25,7 @@ Engine-only install drops this file; reflect Accepted global rules land here. # Session hygiene (apply everywhere, every project) - When a session pivots to a genuinely unrelated task (a different incident, a different deliverable, nothing left in common with what came before), suggest a `/clear` or a fresh session before starting the new work, rather than letting one long session carry unrelated context forward silently. Every turn re-sends the whole conversation, so the finished task's context is paid for again (as cache reads) on every turn of the new one and competes with it for attention. Suggest the reset; do not enforce it. -- When I pivot off an in-flight plan, immediately park the partial tree (`git stash push -u -m "abandoned: "` or a WIP branch) and tell me where it went — never leave a mixed broken working tree silently. Abandoned edits left in place leak into the next task's diffs, test runs, and commits, where they read as part of the new work. +- When I pivot off an in-flight plan, immediately park the partial tree — commit it to a WIP branch, or leave it in its own worktree, and never `git stash` a checkout another session or agent may also be writing to — and tell me where it went — never leave a mixed broken working tree silently. Abandoned edits left in place leak into the next task's diffs, test runs, and commits, where they read as part of the new work. # Live-demo rules (apply everywhere — any time I am physically in the loop: testing, filming, or on a live call) diff --git a/corpus/skills/cat-mode/SKILL.md b/corpus/skills/cat-mode/SKILL.md index 25db6e54..fbb47033 100644 --- a/corpus/skills/cat-mode/SKILL.md +++ b/corpus/skills/cat-mode/SKILL.md @@ -12,7 +12,7 @@ disable-model-invocation: true # cat-mode -Personal conventions, not a task-specific skill. Response shape and brevity live in `diu` (always-on); nothing here duplicates it. Applied by default when `CATSTACK_CAT_MODE_DEFAULT=1` via the `cat-mode-default` hook. +Personal conventions, not a task-specific skill. Response shape and brevity live in `diu` (always-on); nothing here duplicates it. Applied by default when `CATSTACK_CAT_MODE_DEFAULT=on` via the `cat-mode-default` hook. ## Autonomy diff --git a/tests/test_cat_mode.py b/tests/test_cat_mode.py index dce6845a..292a4a70 100644 --- a/tests/test_cat_mode.py +++ b/tests/test_cat_mode.py @@ -95,7 +95,7 @@ def test_description_is_a_real_trigger_not_a_placeholder(self): class TestCatModeDefaultHookPointer(unittest.TestCase): def test_body_names_the_default_hook_and_flag(self): text = normalized_skill_text() - self.assertIn("Applied by default when `CATSTACK_CAT_MODE_DEFAULT=1` via the `cat-mode-default` hook", text) + self.assertIn("Applied by default when `CATSTACK_CAT_MODE_DEFAULT=on` via the `cat-mode-default` hook", text) self.assertTrue(os.path.isdir(os.path.join(REPO_ROOT, "engine", "hooks", "cat-mode-default"))) self.assertEqual(parse_frontmatter(read_skill_text())["disable-model-invocation"], "true") diff --git a/tests/test_install.py b/tests/test_install.py index b771ac0f..bbb974db 100644 --- a/tests/test_install.py +++ b/tests/test_install.py @@ -656,7 +656,8 @@ def test_cursor_session_hygiene_rule_generated_from_learned_rules(self): for bullet in learned_section_bullets("# Session hygiene"): self.assertIn(bullet, text) self.assertIn("fresh session", text) - self.assertIn("git stash push", text) + self.assertIn("WIP branch", text) + self.assertNotIn("git stash push", text) def test_pr_skill_commands_symlinked_for_claude_cursor_and_codex(self): for agent_dir in (".cursor", ".claude", ".codex"): diff --git a/tests/test_parking_rule_never_stashes.py b/tests/test_parking_rule_never_stashes.py new file mode 100644 index 00000000..e12f51f8 --- /dev/null +++ b/tests/test_parking_rule_never_stashes.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 +"""Parking abandoned work must never mean `git stash` in an always-loaded rule. + +cat-mode forbids stashing the primary checkout, because another session or +agent may be writing to it; a stash there takes their edits too. The learned +Session hygiene rule once told the agent to park with `git stash push`, so the +two always-loaded surfaces disagreed and the agent could follow either. +""" +import glob +import os +import unittest + +REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +LEARNED = os.path.join(REPO_ROOT, "corpus", "CLAUDE.learned.md") + + +def always_loaded_surfaces(): + patterns = [ + "engine/CLAUDE.core.md", + "corpus/CLAUDE.learned.md", + "corpus/skills/cat-mode/SKILL.md", + "corpus/skills/cat-mode/references/*.md", + "always-on/*.md", + "cursor/rules/*.mdc", + ] + paths = [] + for pattern in patterns: + paths.extend(sorted(glob.glob(os.path.join(REPO_ROOT, pattern)))) + return paths + + +def session_hygiene_section(): + with open(LEARNED, encoding="utf-8") as handle: + text = handle.read() + start = text.index("# Session hygiene") + end = text.index("\n# ", start + 1) + return text[start:end] + + +class TestParkingRuleNeverStashes(unittest.TestCase): + def test_surfaces_exist(self): + self.assertGreaterEqual(len(always_loaded_surfaces()), 3) + + def test_no_always_loaded_rule_recommends_git_stash_push(self): + for path in always_loaded_surfaces(): + with open(path, encoding="utf-8") as handle: + text = handle.read() + with self.subTest(path=os.path.relpath(path, REPO_ROOT)): + self.assertNotIn("git stash push", text) + + def test_session_hygiene_parks_on_a_branch_or_worktree(self): + section = session_hygiene_section() + self.assertIn("WIP branch", section) + self.assertIn("worktree", section) + self.assertIn("never `git stash`", section) + + +if __name__ == "__main__": + unittest.main()