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
2 changes: 1 addition & 1 deletion skills/.manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@
"self_improve.py": "74c8b9cb0ce81f8600d35e72b73e543bdeccb3c8e5a4cdc79ddb4df5282c1846",
"shift_report.py": "2c4bc669dec391f18329e0fe7b955c6b911322404e7e627e102776c22b448da2",
"skill_forge.py": "07ea8fb2d026eca050154e5e60ea1968e77bc2d7786958d60f6e1a6d50f07182",
"standing_rules.py": "9f7f4a2af7cf8fbe11aaa11440c61eeab299e28da0e3bb0d7a52703787e22ee1",
"standing_rules.py": "1a3faa0da2213d5dba91625a34c8485190363eeef330b1ead23a0ca9bc0e45ea",
"stuck.py": "dfddfe4dfa1a9d5c017f53e53033a7eb33114a517cd6fa937d9d44e65083f1c9",
"system_info.py": "4cc32e0ad9cb81f34309734ef3388f7cde63407de9b23bb0cd589571a4c43ecb",
"terminal.py": "1fe6c1cd1241ea6d328401f1c0fb4c362482e3ec2a61a4f7592b2d4822cd51c1",
Expand Down
28 changes: 20 additions & 8 deletions skills/standing_rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,24 +19,36 @@
"reply, in every session. Persisted to disk, not just remembered in chat."
)
SKILL_MCP_EXPOSE = True
# Commands only — never bare mentions. "standing rule(s)" and "my rules" used to
# be triggers, so merely DISCUSSING standing rules hijacked the turn: a message
# quoting "…was there ever a rule you had to delete?" fired this skill, which
# then tried to remove a rule named "you" and the reply never reached the LLM.
# Same class as the file_ops chat-hang (#282): a mention is not an instruction.
SKILL_TRIGGERS = [
"add a standing rule",
"add standing rule",
"standing rule",
"standing rules",
"my rules",
"always remember to",
"from now on always",
"remove standing rule",
"delete standing rule",
"list standing rules",
"show standing rules",
"show my standing rules",
"what are my standing rules",
"clear standing rules",
]

_ADD = re.compile(
r"^(?:add\s+(?:a\s+)?standing\s+rule|standing\s+rule|from\s+now\s+on\s+always|"
r"always\s+remember\s+to)\b[:\s]*(.+)$", re.I | re.S)
# All anchored to the START of the message. Unanchored, any sentence CONTAINING
# these words matched — "was there ever a rule you had to delete?" read as a
# delete command. A command is what the message opens with, not what it mentions.
_REMOVE = re.compile(
r"\b(?:remove|delete|drop|forget)\b.*\b(?:standing\s+)?rule\b\s*#?\s*(\w+)?", re.I)
r"^\s*(?:remove|delete|drop|forget)\s+(?:the\s+)?(?:standing\s+)?rule\s*#?\s*(\w+)",
re.I)
_LIST = re.compile(
r"\b(?:list|show|what(?:'s| are)|see)\b.*\b(?:standing\s+)?rules?\b", re.I)
_CLEAR = re.compile(r"\b(?:clear|wipe|reset)\b.*\b(?:standing\s+)?rules\b", re.I)
r"^\s*(?:list|show|what(?:'s| are)|see)\b[^.?!]{0,20}\b(?:standing\s+)?rules?\b", re.I)
_CLEAR = re.compile(
r"^\s*(?:clear|wipe|reset)\s+(?:all\s+)?(?:my\s+)?(?:standing\s+)?rules\b", re.I)


def _render(rules):
Expand Down
45 changes: 45 additions & 0 deletions tests/test_standing_rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,3 +187,48 @@ def test_listing_labels_injection_honestly(monkeypatch, tmp_path):
assert "sent to the model" in out
assert "counts INJECTION" in out
assert "fired" not in out.lower(), "must not claim the rule 'fired'"


# ── mentions must not hijack the turn ─────────────────────────────────────────
# "standing rule(s)" and "my rules" were bare triggers, so merely DISCUSSING
# standing rules fired the skill. Quoting "…was there ever a rule you had to
# delete?" matched the unanchored remove-regex, the skill tried to remove a rule
# named "you", and the turn never reached the LLM — so the reply was a skill
# error and the claim/premise checks never ran. Same class as the file_ops
# chat-hang (#282): a mention is not an instruction.

@pytest.mark.parametrize("text", [
"was there ever a rule you had to delete?",
"In that cleanup process, was there ever a standing rule you thought was important?",
"I like the idea of standing rules for agents.",
"You mentioned you delete the rules that never fire.",
"Rules you never invoke are just noise you pay for on every message.",
"he ends each session by listing which standing rules actually fired",
])
def test_mentions_do_not_dispatch(text):
import codec_dispatch as cd
cd.load_skills()
hit = cd.check_skill(text)
assert (hit or {}).get("name") != "standing_rules", f"hijacked by: {text!r}"


@pytest.mark.parametrize("text", [
"add a standing rule: be brief",
"show my standing rules",
"list standing rules",
"remove standing rule 2",
])
def test_real_commands_still_dispatch(text):
import codec_dispatch as cd
cd.load_skills()
hit = cd.check_skill(text)
assert (hit or {}).get("name") == "standing_rules", f"command missed: {text!r}"


def test_remove_regex_requires_a_leading_command(monkeypatch, tmp_path):
s = _skill(monkeypatch, tmp_path)
s.run("add a standing rule: keep me")
# a question that merely mentions deleting must not remove anything
s.run("was there ever a rule you had to delete?")
import codec_standing_rules as mod
assert len(mod.list_rules()) == 1, "a mention must never delete a rule"