Skip to content
Closed
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
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
2 changes: 1 addition & 1 deletion corpus/CLAUDE.learned.md
Original file line number Diff line number Diff line change
Expand Up @@ -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: <plan>"` 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)

Expand Down
2 changes: 1 addition & 1 deletion corpus/skills/cat-mode/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion tests/test_cat_mode.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down
3 changes: 2 additions & 1 deletion tests/test_install.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"):
Expand Down
59 changes: 59 additions & 0 deletions tests/test_parking_rule_never_stashes.py
Original file line number Diff line number Diff line change
@@ -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()
Loading