switchroom 0.17.6 → 0.17.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-scheduler/index.js +38 -4
- package/dist/auth-broker/index.js +302 -203
- package/dist/cli/notion-write-pretool.mjs +35 -2
- package/dist/cli/switchroom.js +1178 -576
- package/dist/host-control/main.js +148 -14
- package/dist/vault/approvals/kernel-server.js +140 -55
- package/dist/vault/broker/server.js +142 -57
- package/package.json +1 -1
- package/profiles/_base/start.sh.hbs +50 -6
- package/profiles/default/CLAUDE.md +116 -0
- package/skills/mental-model-curator/SKILL.md +162 -0
- package/telegram-plugin/bridge/bridge.ts +80 -1
- package/telegram-plugin/bridge/ipc-client.ts +19 -0
- package/telegram-plugin/bridge/permission-ledger.ts +61 -0
- package/telegram-plugin/consolidation-legibility.ts +279 -0
- package/telegram-plugin/dist/bridge/bridge.js +85 -1
- package/telegram-plugin/dist/gateway/gateway.js +2565 -610
- package/telegram-plugin/dist/server.js +86 -2
- package/telegram-plugin/feed-heartbeat-climb.ts +206 -0
- package/telegram-plugin/gateway/activity-card-store.ts +293 -0
- package/telegram-plugin/gateway/gateway.ts +1376 -82
- package/telegram-plugin/gateway/inbound-spool.ts +22 -0
- package/telegram-plugin/gateway/mental-model-propose-card.ts +69 -0
- package/telegram-plugin/gateway/mental-model-propose-diff.ts +171 -0
- package/telegram-plugin/gateway/mental-model-propose-inbound-builders.ts +147 -0
- package/telegram-plugin/gateway/mental-model-propose-resolve.ts +201 -0
- package/telegram-plugin/gateway/missed-approvals-card.ts +161 -0
- package/telegram-plugin/gateway/missed-approvals-store.ts +167 -0
- package/telegram-plugin/gateway/permission-rearm.ts +115 -0
- package/telegram-plugin/gateway/scoped-grant-store.ts +89 -0
- package/telegram-plugin/memory-legibility.ts +217 -0
- package/telegram-plugin/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/telegram-plugin/scoped-approval.ts +59 -0
- package/telegram-plugin/silent-end.ts +78 -0
- package/telegram-plugin/subagent-watcher.ts +60 -6
- package/telegram-plugin/tests/activity-card-store.test.ts +436 -0
- package/telegram-plugin/tests/activity-card-wiring.test.ts +88 -0
- package/telegram-plugin/tests/consolidation-legibility.test.ts +224 -0
- package/telegram-plugin/tests/emission-authority-facade.test.ts +25 -10
- package/telegram-plugin/tests/feed-heartbeat-liveness-open.test.ts +33 -9
- package/telegram-plugin/tests/gateway-boot-marker-clear.test.ts +3 -3
- package/telegram-plugin/tests/inbound-spool.test.ts +105 -0
- package/telegram-plugin/tests/memory-legibility.test.ts +216 -0
- package/telegram-plugin/tests/mental-model-propose-callback-gate.test.ts +67 -0
- package/telegram-plugin/tests/mental-model-propose-card.test.ts +56 -0
- package/telegram-plugin/tests/mental-model-propose-diff.test.ts +201 -0
- package/telegram-plugin/tests/mental-model-propose-inbound-builders.test.ts +68 -0
- package/telegram-plugin/tests/mental-model-propose-resolve.test.ts +157 -0
- package/telegram-plugin/tests/missed-approvals-card.test.ts +145 -0
- package/telegram-plugin/tests/missed-approvals-store.test.ts +147 -0
- package/telegram-plugin/tests/missed-approvals-wiring.test.ts +89 -0
- package/telegram-plugin/tests/permission-ledger.test.ts +166 -0
- package/telegram-plugin/tests/permission-no-repeat-wiring.test.ts +1 -1
- package/telegram-plugin/tests/permission-rearm-wiring.test.ts +175 -0
- package/telegram-plugin/tests/permission-rearm.test.ts +126 -0
- package/telegram-plugin/tests/scoped-grant-persist.test.ts +223 -0
- package/telegram-plugin/tests/silent-end-transport.test.ts +290 -0
- package/telegram-plugin/tests/silent-turn-climb-transport.test.ts +337 -0
- package/telegram-plugin/tests/subagent-watcher.test.ts +139 -0
- package/telegram-plugin/tests/worktree-watch-cwds.test.ts +103 -0
- package/telegram-plugin/uat/assertions.ts +88 -4
- package/telegram-plugin/uat/feed-matcher.test.ts +69 -0
- package/telegram-plugin/uat/scenarios/fuzz-liveness-climb-dm.test.ts +155 -0
- package/telegram-plugin/uat/scenarios/jtbd-directive-capture-nudge-dm.test.ts +185 -0
- package/telegram-plugin/uat/scenarios/jtbd-liveness-climb-channel.test.ts +192 -0
- package/telegram-plugin/uat/scenarios/jtbd-liveness-climb-dm.test.ts +220 -0
- package/telegram-plugin/uat/scenarios/jtbd-liveness-narration-channel.test.ts +137 -0
- package/telegram-plugin/uat/scenarios/jtbd-liveness-narration-dm.test.ts +148 -0
- package/telegram-plugin/uat/scenarios/jtbd-memory-legibility-channel.test.ts +66 -0
- package/telegram-plugin/uat/scenarios/jtbd-memory-legibility-dm.test.ts +61 -0
- package/telegram-plugin/uat/scenarios/silent-end-recovery-channel.test.ts +136 -0
- package/telegram-plugin/uat/scenarios/silent-end-recovery-dm.test.ts +24 -2
- package/telegram-plugin/worktree-watch-cwds.ts +60 -0
- package/vendor/hindsight-memory/hooks/hooks.json +9 -0
- package/vendor/hindsight-memory/scripts/__pycache__/directive_verify.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/__pycache__/drain_pending.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/__pycache__/recall.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/__pycache__/retain.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/__pycache__/session_end.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/directive_verify.py +445 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/__init__.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/bank.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/client.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/config.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/content.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/daemon.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/directives.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/gateway_ipc.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/llm.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/pending.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/state.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/switchroom_envelope.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/config.py +37 -0
- package/vendor/hindsight-memory/scripts/lib/directives.py +88 -0
- package/vendor/hindsight-memory/scripts/lib/switchroom_envelope.py +77 -0
- package/vendor/hindsight-memory/scripts/recall.py +153 -4
- package/vendor/hindsight-memory/scripts/retain.py +17 -0
- package/vendor/hindsight-memory/scripts/setup_hooks.py +9 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/__init__.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_config_client_casts.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_config_client_casts.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_directive_capture_nudge.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_directive_capture_nudge.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_directive_verify.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_directive_verify.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_directives.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_directives.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_gateway_ipc.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_gateway_ipc.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_context_slice.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_context_slice.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_integration.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_integration.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_tag_filters.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_tag_filters.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_topic_filter.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_topic_filter.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_trivial_skip.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_trivial_skip.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_retain_window.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_retain_window.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_sender_routing.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_sender_routing.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_switchroom_envelope.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/test_directive_capture_nudge.py +185 -0
- package/vendor/hindsight-memory/scripts/tests/test_directive_verify.py +516 -0
- package/vendor/hindsight-memory/scripts/tests/test_directives.py +49 -0
- package/vendor/hindsight-memory/scripts/tests/test_retain_window.py +66 -1
- package/vendor/hindsight-memory/scripts/tests/test_switchroom_envelope.py +69 -0
- package/vendor/hindsight-memory/tests/__pycache__/conftest.cpython-313-pytest-9.0.3.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/conftest.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_bank.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_bank.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_client.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_client.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_config.cpython-313-pytest-9.0.3.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_config.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_config.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_content.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_content.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_drain_pending.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_drain_pending.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_hooks.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_hooks.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_manifest.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_manifest.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_pending.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_pending.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_recall_exit_codes.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_recall_exit_codes.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_session_end_pending.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_session_end_pending.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_state.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_state.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/tests/test_recall_exit_codes.py +49 -2
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Switchroom gateway inbound-envelope grammar — the ONE place this vendored
|
|
2
|
+
Python knows about switchroom's ``<channel source="...">`` wire format.
|
|
3
|
+
|
|
4
|
+
SWITCHROOM DIVERGENCE (not upstream Hindsight). This module isolates the
|
|
5
|
+
coupling between the vendored Python hooks and the switchroom telegram-plugin
|
|
6
|
+
gateway's inbound envelope. It exists so a change to the TS-side envelope
|
|
7
|
+
grammar has ONE obvious Python counterpart to update — instead of the grammar
|
|
8
|
+
being buried inline inside directive_verify.py where a silent TS format change
|
|
9
|
+
could break a Python guard undetected (#2903 Fix 6.3).
|
|
10
|
+
|
|
11
|
+
The gateway prepends a leading ``<channel source="..." ...>`` envelope to every
|
|
12
|
+
SYNTHESIZED / non-human inbound it injects into the model (cron fires, reaction
|
|
13
|
+
dispatches, resume synthetics, vault-grant approvals, sub-agent handbacks,
|
|
14
|
+
obligation representations, …). See, on the TS side:
|
|
15
|
+
|
|
16
|
+
* telegram-plugin/gateway/*-inbound-builder.ts (and the reaction / resume /
|
|
17
|
+
vault-grant / subagent / obligation builders) — they emit
|
|
18
|
+
``<channel source="<name>" ...>``.
|
|
19
|
+
* A genuine human message from the primary channel carries
|
|
20
|
+
``source="telegram"``; interactive (non-gateway) sessions carry NO envelope
|
|
21
|
+
at all.
|
|
22
|
+
|
|
23
|
+
Contract pinned here (keep in lockstep with the gateway):
|
|
24
|
+
* ``CHANNEL_SOURCE_RE`` — extracts the ``source`` attribute from the leading
|
|
25
|
+
envelope. Handles both open (``<channel source="telegram" ...>``) and
|
|
26
|
+
self-closing (``<channel source="reaction"/>``) forms.
|
|
27
|
+
* ``HUMAN_INBOUND_SOURCES`` — the whitelist of ``source`` values that denote a
|
|
28
|
+
GENUINE human turn. A whitelist (not a blacklist) so any NEW synthetic
|
|
29
|
+
source the gateway adds is treated as machine-authored by default rather
|
|
30
|
+
than accidentally classified human.
|
|
31
|
+
|
|
32
|
+
If the gateway ever renames the envelope tag, changes the attribute name, or
|
|
33
|
+
adds a new human-facing channel, update THIS module (and its test,
|
|
34
|
+
tests/test_switchroom_envelope.py). Nothing else in the Python tree should hard
|
|
35
|
+
-code the ``<channel source=`` grammar.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
import re
|
|
39
|
+
|
|
40
|
+
# Genuine human inbound channels. A "user" turn whose `<channel source="...">`
|
|
41
|
+
# envelope names anything else is a SYNTHESIZED / cron inbound built by the
|
|
42
|
+
# gateway — a machine turn, NOT a human correction.
|
|
43
|
+
HUMAN_INBOUND_SOURCES = frozenset({"telegram"})
|
|
44
|
+
|
|
45
|
+
# Pulls the source attribute out of the gateway's leading `<channel ...>`
|
|
46
|
+
# envelope. Handles both open (`<channel source="telegram" ...>`) and
|
|
47
|
+
# self-closing (`<channel source="reaction"/>`) forms.
|
|
48
|
+
CHANNEL_SOURCE_RE = re.compile(r"""<channel\b[^>]*\bsource=["']([^"']+)["']""")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def envelope_source(text) -> "str | None":
|
|
52
|
+
"""Return the lower-cased ``source`` of the leading channel envelope, or
|
|
53
|
+
None when there is no envelope (interactive session). Pure regex."""
|
|
54
|
+
if not isinstance(text, str):
|
|
55
|
+
return None
|
|
56
|
+
m = CHANNEL_SOURCE_RE.search(text)
|
|
57
|
+
if not m:
|
|
58
|
+
return None
|
|
59
|
+
return m.group(1).strip().lower()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def is_synthetic_inbound(text) -> bool:
|
|
63
|
+
"""True when this turn's opening message is a synthesized/cron inbound
|
|
64
|
+
rather than a genuine human message.
|
|
65
|
+
|
|
66
|
+
Detection is purely on the gateway-prepended ``<channel source="...">``
|
|
67
|
+
envelope: a ``source`` outside ``HUMAN_INBOUND_SOURCES`` (``cron``,
|
|
68
|
+
``reaction``, ``resume_interrupted``, ``vault_grant_approved``,
|
|
69
|
+
``subagent_handback``, ``obligation_represent``, …) marks a non-human turn.
|
|
70
|
+
No envelope (interactive session) → treated as human. Injected channel tags
|
|
71
|
+
in a human's own body are neutralised upstream by the channel-envelope
|
|
72
|
+
sanitiser, so the only real ``<channel source=`` is the gateway's.
|
|
73
|
+
"""
|
|
74
|
+
source = envelope_source(text)
|
|
75
|
+
if source is None:
|
|
76
|
+
return False
|
|
77
|
+
return source not in HUMAN_INBOUND_SOURCES
|
|
@@ -597,6 +597,128 @@ def _is_trivial_stateless(ack_form, stripped):
|
|
|
597
597
|
return False
|
|
598
598
|
|
|
599
599
|
|
|
600
|
+
# Switchroom #2848 Stage B — deterministic directive-capture nudge.
|
|
601
|
+
#
|
|
602
|
+
# Stage A audit (issue #2848) measured a ~55% miss rate on durable
|
|
603
|
+
# corrections: the model is instructed (guidance-only, in
|
|
604
|
+
# profiles/default/CLAUDE.md.hbs) to persist standing rules with
|
|
605
|
+
# mcp__hindsight__create_directive, but does so inconsistently — capture
|
|
606
|
+
# is a per-agent lottery (the same broadcast correction was captured by
|
|
607
|
+
# one agent and silently dropped by two others). This adds DETERMINISTIC
|
|
608
|
+
# detection of correction / standing-rule-shaped inbound (pure regex — NO
|
|
609
|
+
# model callsite; the claude-native invariant forbids a classifier call)
|
|
610
|
+
# and appends a terse advisory nudge to the UserPromptSubmit
|
|
611
|
+
# additionalContext. The MODEL makes the judgment IN-SESSION and calls
|
|
612
|
+
# create_directive itself (visible in chat); the hook NEVER writes a
|
|
613
|
+
# directive on its own — a silent hook-side write would break
|
|
614
|
+
# chat-legibility and edge the no-self-escalation invariant.
|
|
615
|
+
#
|
|
616
|
+
# On by default (Stage A proved a real gap → defaults principle);
|
|
617
|
+
# operators opt out per-agent via memory.directive_capture_nudge=false in
|
|
618
|
+
# switchroom.yaml, which start.sh exports as
|
|
619
|
+
# HINDSIGHT_DIRECTIVE_CAPTURE_NUDGE=false (recall.py falls back to the
|
|
620
|
+
# settings.json default, which switchroom pins on).
|
|
621
|
+
#
|
|
622
|
+
# Detection is intentionally inclusive (the nudge is cheap + advisory, so
|
|
623
|
+
# a modest false-positive rate is acceptable — the model just ignores it
|
|
624
|
+
# on a one-off), but a negative guard scrubs the obvious pleasantry
|
|
625
|
+
# shapes ("always happy to help", "never mind") BEFORE the positive
|
|
626
|
+
# match, so those don't fire on a bare "always"/"never".
|
|
627
|
+
_DIRECTIVE_NUDGE_NEGATIVE_RE = re.compile(
|
|
628
|
+
r"""(?ix)
|
|
629
|
+
(?:
|
|
630
|
+
never \s* mind
|
|
631
|
+
| nevermind
|
|
632
|
+
| always \s+ (?: happy | glad | welcome | here \s+ to \s+ help | open \s+ to )
|
|
633
|
+
| never \s+ (?: fails? | hurts? | too \s+ late | a \s+ dull )
|
|
634
|
+
| as \s+ always
|
|
635
|
+
| thanks? \s+ as \s+ always
|
|
636
|
+
| forever \s+ grateful
|
|
637
|
+
)
|
|
638
|
+
"""
|
|
639
|
+
)
|
|
640
|
+
|
|
641
|
+
_DIRECTIVE_NUDGE_RE = re.compile(
|
|
642
|
+
r"""(?ix)
|
|
643
|
+
(?:
|
|
644
|
+
# --- explicit standing-rule framings ---
|
|
645
|
+
\b from \s+ now \s+ on \b
|
|
646
|
+
| \b going \s+ forward \b
|
|
647
|
+
| \b in \s+ (?: the \s+ )? future \b
|
|
648
|
+
| \b (?: as \s+ a \s+ )? general \s+ (?: rule | behaviou?r | policy | principle | agent \s+ behaviou?r ) \b
|
|
649
|
+
| \b as \s+ a \s+ rule \b
|
|
650
|
+
| \b i \s+ want \s+ you \s+ to \b
|
|
651
|
+
| \b you \s+ should \s+ (?: always | never ) \b
|
|
652
|
+
# --- always / never (pleasantries pre-scrubbed by the negative guard) ---
|
|
653
|
+
| \b always \b
|
|
654
|
+
| \b never \b
|
|
655
|
+
| \b no \s+ longer \b
|
|
656
|
+
| \b anymore \b
|
|
657
|
+
# --- prohibitions / behavioural corrections ---
|
|
658
|
+
| \b stop \s+ (?: doing | saying | using | calling | adding | making | being | sending | putting ) \b
|
|
659
|
+
| \b (?: do \s* n['’]? t | don['’]? t | do \s+ not ) \s+ (?: ever \s+ )?
|
|
660
|
+
(?: make | guess | assume | use | say | add | send | call | include | do | put | write | reply | respond | mention ) \b
|
|
661
|
+
| \b (?: do \s* n['’]? t | don['’]? t | do \s+ not ) \b [^.?!]{0,40} \b again \b
|
|
662
|
+
| \b (?: should \s* n['’]? t | shouldn['’]? t | should \s+ not ) \b
|
|
663
|
+
# --- preferences / identity ---
|
|
664
|
+
| \b (?: i | we ) \s+ (?: prefer | always \s+ want | never \s+ want ) \b
|
|
665
|
+
| \b i['’]? d \s+ prefer \b
|
|
666
|
+
| \b call \s+ me \b
|
|
667
|
+
# --- memory / reinforcement ---
|
|
668
|
+
| \b remember \s+ (?: to | that | this | always | never ) \b
|
|
669
|
+
| \b make \s+ sure \s+ (?: you | to | that | not | it ) \b
|
|
670
|
+
# --- explicit wrongness + restatement ---
|
|
671
|
+
| \b that['’]? s \s+ (?: wrong | not \s+ right | incorrect | not \s+ what ) \b
|
|
672
|
+
| \b (?: no ,? \s+ )? it['’]? s \s+ (?: actually | not ) \b
|
|
673
|
+
| \b (?: that['’]? s | it['’]? s ) \s+ (?: dead | deprecated | retired | closed ) \b
|
|
674
|
+
| \b we \s+ (?: now \s+ )? (?: use \s+ only | only \s+ use | no \s+ longer \s+ use ) \b
|
|
675
|
+
)
|
|
676
|
+
"""
|
|
677
|
+
)
|
|
678
|
+
|
|
679
|
+
# Terse, advisory. The model decides IN-SESSION whether this is a durable
|
|
680
|
+
# rule and, if so, calls create_directive itself (chat-legible). Kept
|
|
681
|
+
# short so it costs a handful of tokens on a false positive.
|
|
682
|
+
_DIRECTIVE_CAPTURE_NUDGE = (
|
|
683
|
+
"<directive_capture_check>\n"
|
|
684
|
+
"The latest user message is correction- or standing-rule-shaped "
|
|
685
|
+
'(e.g. "always/never …", "from now on …", "stop doing …", a stated '
|
|
686
|
+
'preference, or "that’s wrong, it’s …"). If it states a DURABLE rule '
|
|
687
|
+
"for how you should behave going forward — not a one-off request for "
|
|
688
|
+
"this task — persist it NOW with mcp__hindsight__create_directive "
|
|
689
|
+
"(verbatim, in the user’s own words) BEFORE you answer, so the "
|
|
690
|
+
"correction survives future sessions. UNLESS an equivalent active "
|
|
691
|
+
"directive already exists (see any <active_directives> block above) — "
|
|
692
|
+
"in that case it is already saved; do NOT create a duplicate. If it’s "
|
|
693
|
+
"only a one-off instruction, ignore this note and just answer.\n"
|
|
694
|
+
"</directive_capture_check>"
|
|
695
|
+
)
|
|
696
|
+
|
|
697
|
+
|
|
698
|
+
def looks_like_standing_rule(text) -> bool:
|
|
699
|
+
"""Deterministic (regex-only) test for correction / standing-rule shape.
|
|
700
|
+
|
|
701
|
+
Pleasantry shapes ("always happy to help", "never mind") are scrubbed
|
|
702
|
+
BEFORE the positive match so they can't trip the bare always/never
|
|
703
|
+
signals. Returns False on empty / non-string input. No model call —
|
|
704
|
+
the model does the actual judgment in-session (issue #2848).
|
|
705
|
+
"""
|
|
706
|
+
if not isinstance(text, str) or not text.strip():
|
|
707
|
+
return False
|
|
708
|
+
scrubbed = _DIRECTIVE_NUDGE_NEGATIVE_RE.sub(" ", text)
|
|
709
|
+
return bool(_DIRECTIVE_NUDGE_RE.search(scrubbed))
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
def _combine_context(base, nudge) -> str:
|
|
713
|
+
"""Join the recall/directives context with the directive-capture nudge,
|
|
714
|
+
skipping empties. Either may be None/empty. The nudge is kept OUT of the
|
|
715
|
+
cached / last-recall context (it's transient per-inbound) and appended
|
|
716
|
+
only at emit time, so a cache hit re-derives it from the current prompt
|
|
717
|
+
rather than replaying a stale one."""
|
|
718
|
+
parts = [p for p in (base, nudge) if p]
|
|
719
|
+
return "\n\n".join(parts)
|
|
720
|
+
|
|
721
|
+
|
|
600
722
|
def main():
|
|
601
723
|
config = load_config()
|
|
602
724
|
|
|
@@ -665,6 +787,22 @@ def main():
|
|
|
665
787
|
debug_log(config, f"Prompt is trivial/stateless ({_ack_form!r}), skipping recall")
|
|
666
788
|
return
|
|
667
789
|
|
|
790
|
+
# Switchroom #2848 Stage B — directive-capture nudge. Deterministic
|
|
791
|
+
# (regex) detection of correction / standing-rule-shaped inbound; when
|
|
792
|
+
# it fires we append a terse advisory to the additionalContext telling
|
|
793
|
+
# the model to persist the rule with create_directive if it IS durable.
|
|
794
|
+
# Computed on `_stripped` (the channel-envelope-stripped text) so the
|
|
795
|
+
# `<channel …>` wrapper never trips detection. Runs AFTER the ack/
|
|
796
|
+
# trivial-skip gates (those return early and genuinely need no nudge)
|
|
797
|
+
# and leaves the recall/directive path below untouched — it only adds
|
|
798
|
+
# to whatever additionalContext that path emits. On by default;
|
|
799
|
+
# HINDSIGHT_DIRECTIVE_CAPTURE_NUDGE=false (or memory.directive_capture_nudge:
|
|
800
|
+
# false) turns it off. No model callsite (claude-native invariant).
|
|
801
|
+
nudge_block = None
|
|
802
|
+
if config.get("directiveCaptureNudge", True) and looks_like_standing_rule(_stripped):
|
|
803
|
+
nudge_block = _DIRECTIVE_CAPTURE_NUDGE
|
|
804
|
+
debug_log(config, "Directive-capture nudge: inbound looks like a standing rule")
|
|
805
|
+
|
|
668
806
|
session_id = hook_input.get("session_id") or ""
|
|
669
807
|
|
|
670
808
|
# Switchroom #303 — push a "📚 recalling memories" status to the
|
|
@@ -771,7 +909,10 @@ def main():
|
|
|
771
909
|
cached_context = None
|
|
772
910
|
if cached_context is not None:
|
|
773
911
|
debug_log(config, f"Recall cache HIT (key={cache_key[:12]}…) — skipping API call")
|
|
774
|
-
|
|
912
|
+
# #2848 — append the nudge to the cached context at emit time
|
|
913
|
+
# (the cache stores nudge-free context; the nudge is re-derived
|
|
914
|
+
# from the current prompt, so a hit can't replay a stale one).
|
|
915
|
+
_emit_cached_context(_combine_context(cached_context, nudge_block))
|
|
775
916
|
_write_recall_log({
|
|
776
917
|
"ts": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
|
777
918
|
"session_id": (session_id or "")[:32],
|
|
@@ -790,6 +931,7 @@ def main():
|
|
|
790
931
|
"active_thread_id": active_thread_id,
|
|
791
932
|
"active_topic_alias": active_topic_alias,
|
|
792
933
|
"topic_filter_mode": _topic_filter_mode(),
|
|
934
|
+
"directive_nudge": bool(nudge_block),
|
|
793
935
|
})
|
|
794
936
|
return
|
|
795
937
|
debug_log(config, f"Recall cache MISS (key={cache_key[:12]}…)")
|
|
@@ -1025,8 +1167,12 @@ def main():
|
|
|
1025
1167
|
update_placeholder(placeholder_chat_id, "💭 thinking")
|
|
1026
1168
|
|
|
1027
1169
|
# If neither block has content, there's nothing to inject — exit
|
|
1028
|
-
# silently to avoid emitting an empty hookSpecificOutput.
|
|
1170
|
+
# silently to avoid emitting an empty hookSpecificOutput. #2848: unless
|
|
1171
|
+
# the directive-capture nudge fired, in which case emit the nudge alone
|
|
1172
|
+
# (a correction with no memories/directives still needs the reminder).
|
|
1029
1173
|
if not directives_block and not memories_block:
|
|
1174
|
+
if nudge_block:
|
|
1175
|
+
_emit_cached_context(nudge_block)
|
|
1030
1176
|
return
|
|
1031
1177
|
|
|
1032
1178
|
# Compose final context. Directives block goes ABOVE memories so the
|
|
@@ -1092,13 +1238,16 @@ def main():
|
|
|
1092
1238
|
"source_topics": source_topic_summary,
|
|
1093
1239
|
"topic_filter_mode": topic_filter_mode,
|
|
1094
1240
|
"topic_dropped": topic_dropped,
|
|
1241
|
+
"directive_nudge": bool(nudge_block),
|
|
1095
1242
|
})
|
|
1096
1243
|
|
|
1097
|
-
# Output JSON for Claude Code hook system
|
|
1244
|
+
# Output JSON for Claude Code hook system. #2848: append the
|
|
1245
|
+
# directive-capture nudge (if it fired) at emit time — it's kept out of
|
|
1246
|
+
# the cached / last-recall context above so it can't go stale.
|
|
1098
1247
|
output = {
|
|
1099
1248
|
"hookSpecificOutput": {
|
|
1100
1249
|
"hookEventName": "UserPromptSubmit",
|
|
1101
|
-
"additionalContext": context_message,
|
|
1250
|
+
"additionalContext": _combine_context(context_message, nudge_block),
|
|
1102
1251
|
}
|
|
1103
1252
|
}
|
|
1104
1253
|
json.dump(output, sys.stdout)
|
|
@@ -147,6 +147,23 @@ def run_retain(hook_input: dict, force: bool = False) -> dict:
|
|
|
147
147
|
|
|
148
148
|
debug_log(config, f"Retain hook_input keys: {list(hook_input.keys())} force={force}")
|
|
149
149
|
|
|
150
|
+
# Blocked-Stop double-fire guard (switchroom #2848 Phase 3): when
|
|
151
|
+
# directive_verify.py blocks a Stop, the model continues and the Stop
|
|
152
|
+
# event fires AGAIN, this time carrying ``stop_hook_active: true``.
|
|
153
|
+
# Retaining on that second fire would (a) POST a duplicate transcript
|
|
154
|
+
# document and (b) at retainEveryNTurns>1 call increment_turn_count a
|
|
155
|
+
# second time for the SAME logical turn — drifting the cadence throttle.
|
|
156
|
+
# Skip the re-fire entirely BEFORE any store or turn-count increment.
|
|
157
|
+
# A forced SessionEnd sweep (force=True) is a distinct hook event and
|
|
158
|
+
# must always flush, so it is exempt.
|
|
159
|
+
if not force and hook_input.get("stop_hook_active"):
|
|
160
|
+
debug_log(
|
|
161
|
+
config,
|
|
162
|
+
"Stop re-fire (stop_hook_active=true) — skipping retain to avoid "
|
|
163
|
+
"duplicate document / turn-count drift",
|
|
164
|
+
)
|
|
165
|
+
return {"status": "skipped", "reason": "stop_hook_active"}
|
|
166
|
+
|
|
150
167
|
session_id = hook_input.get("session_id", "unknown")
|
|
151
168
|
transcript_path = hook_input.get("transcript_path", "")
|
|
152
169
|
|
|
Binary file
|
|
Binary file
|
package/vendor/hindsight-memory/scripts/tests/__pycache__/test_config_client_casts.cpython-313.pyc
ADDED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/vendor/hindsight-memory/scripts/tests/__pycache__/test_directive_verify.cpython-313.pyc
ADDED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_context_slice.cpython-313.pyc
ADDED
|
Binary file
|
|
Binary file
|
package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_integration.cpython-313.pyc
ADDED
|
Binary file
|
|
Binary file
|
package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_tag_filters.cpython-313.pyc
ADDED
|
Binary file
|
|
Binary file
|
package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_topic_filter.cpython-313.pyc
ADDED
|
Binary file
|
|
Binary file
|
package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_trivial_skip.cpython-313.pyc
ADDED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"""Switchroom #2848 Stage B — unit tests for the directive-capture nudge.
|
|
2
|
+
|
|
3
|
+
Stage A audit measured a ~55% miss rate on durable corrections: agents are
|
|
4
|
+
told (guidance-only) to persist standing rules with
|
|
5
|
+
`mcp__hindsight__create_directive`, but capture is a per-agent lottery. Stage B
|
|
6
|
+
adds DETERMINISTIC (regex) detection of correction / standing-rule-shaped
|
|
7
|
+
inbound in recall.py's UserPromptSubmit hook and appends a terse advisory nudge
|
|
8
|
+
to the turn's additionalContext — the model does the actual judgment in-session
|
|
9
|
+
(no model callsite; claude-native invariant).
|
|
10
|
+
|
|
11
|
+
These tests pin:
|
|
12
|
+
* True positives — the audit's real MISSES + the issue's listed shapes.
|
|
13
|
+
* True negatives — pleasantries and questions must NOT fire (guard against
|
|
14
|
+
"always happy to help" / "never mind" tripping the bare always/never).
|
|
15
|
+
* The nudge is APPENDED to context without disturbing recall output
|
|
16
|
+
(_combine_context), and the nudge string is the create_directive advisory.
|
|
17
|
+
* The trivial-stateless recall gate is unaffected — a correction is never
|
|
18
|
+
trivial-skipped, and greetings still are (no regression on recall.py:582).
|
|
19
|
+
|
|
20
|
+
Stdlib-only.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import os
|
|
24
|
+
import sys
|
|
25
|
+
import unittest
|
|
26
|
+
|
|
27
|
+
SCRIPTS_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
|
28
|
+
if SCRIPTS_DIR not in sys.path:
|
|
29
|
+
sys.path.insert(0, SCRIPTS_DIR)
|
|
30
|
+
|
|
31
|
+
from recall import ( # noqa: E402
|
|
32
|
+
_DIRECTIVE_CAPTURE_NUDGE,
|
|
33
|
+
_combine_context,
|
|
34
|
+
_is_trivial_stateless,
|
|
35
|
+
looks_like_standing_rule,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# Correction / standing-rule-shaped inbound that MUST fire the nudge.
|
|
40
|
+
# The first block is drawn verbatim (paraphrased) from Stage A's clear MISSES
|
|
41
|
+
# — the exact corrections the guidance-only path silently dropped.
|
|
42
|
+
CORRECTIONS = [
|
|
43
|
+
# --- Stage A clear misses ---
|
|
44
|
+
"you should always hyperlink linear issues when mentioning",
|
|
45
|
+
"i also want you to always provide a direct link to open the email",
|
|
46
|
+
"i want this behaviour of validate, dont assume as a general agent behaviour",
|
|
47
|
+
"don't make stuff up, investigate and validate",
|
|
48
|
+
"validate not assume, don't guess",
|
|
49
|
+
"We shouldn't publish confidential Buildkite data, so don't use names",
|
|
50
|
+
"The repo mekenthompson/switchroom is closed/dead. We use only switchroom/switchroom now",
|
|
51
|
+
# --- Stage A true captures (must also detect — the loop should fire on these) ---
|
|
52
|
+
"i never use them",
|
|
53
|
+
"We don't use streaming reply anymore",
|
|
54
|
+
"i always want you to help better format articles for readability",
|
|
55
|
+
# --- the issue's listed standing-rule shapes ---
|
|
56
|
+
"from now on, prefer British spelling",
|
|
57
|
+
"going forward, use GFM syntax",
|
|
58
|
+
"call me Ken, not Kenneth",
|
|
59
|
+
"stop doing that",
|
|
60
|
+
"stop using em-dashes",
|
|
61
|
+
"don't do that again",
|
|
62
|
+
"remember to close the loop with a summary",
|
|
63
|
+
"make sure you cite the file and line",
|
|
64
|
+
"make sure not to leak the token",
|
|
65
|
+
"no, it's actually the blue one",
|
|
66
|
+
"that's wrong",
|
|
67
|
+
"that's not what I asked for",
|
|
68
|
+
"I'd prefer you keep replies short",
|
|
69
|
+
"we prefer concise answers",
|
|
70
|
+
"in the future, ping me before deploying",
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
# Pleasantries, one-off questions, acks — must NOT fire (false positives cost
|
|
74
|
+
# tokens + noise; the negative guard scrubs pleasantry shapes first).
|
|
75
|
+
NON_CORRECTIONS = [
|
|
76
|
+
"always happy to help!",
|
|
77
|
+
"I'm always glad to assist",
|
|
78
|
+
"never mind, forget it",
|
|
79
|
+
"nevermind that",
|
|
80
|
+
"thanks as always",
|
|
81
|
+
"as always, appreciate it",
|
|
82
|
+
"can you check the deploy status",
|
|
83
|
+
"what time is it",
|
|
84
|
+
"how do I fix this bug",
|
|
85
|
+
"tell me about the project",
|
|
86
|
+
"the CI issue is now closed", # 'closed' alone, not "it's/that's closed"
|
|
87
|
+
"please run the tests",
|
|
88
|
+
"what's the weather like",
|
|
89
|
+
# The <channel …> envelope wrapper on its own must never trigger.
|
|
90
|
+
'<channel user="ken" chat_id="123">',
|
|
91
|
+
]
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class TestDirectiveCaptureDetection(unittest.TestCase):
|
|
95
|
+
def test_corrections_fire_the_nudge(self):
|
|
96
|
+
for p in CORRECTIONS:
|
|
97
|
+
with self.subTest(prompt=p):
|
|
98
|
+
self.assertTrue(
|
|
99
|
+
looks_like_standing_rule(p),
|
|
100
|
+
f"expected standing-rule detection for {p!r}",
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
def test_pleasantries_and_questions_do_not_fire(self):
|
|
104
|
+
for p in NON_CORRECTIONS:
|
|
105
|
+
with self.subTest(prompt=p):
|
|
106
|
+
self.assertFalse(
|
|
107
|
+
looks_like_standing_rule(p),
|
|
108
|
+
f"FALSE POSITIVE: nudge would fire for {p!r}",
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
def test_empty_and_non_string_are_false(self):
|
|
112
|
+
for bad in ("", " ", None, 123, [], {}):
|
|
113
|
+
with self.subTest(value=bad):
|
|
114
|
+
self.assertFalse(looks_like_standing_rule(bad))
|
|
115
|
+
|
|
116
|
+
def test_pleasantry_scrub_does_not_mask_a_real_rule(self):
|
|
117
|
+
# A message that opens with a pleasantry AND states a real rule must
|
|
118
|
+
# still fire — the negative guard scrubs only the pleasantry span.
|
|
119
|
+
self.assertTrue(
|
|
120
|
+
looks_like_standing_rule(
|
|
121
|
+
"always happy to help — but from now on use British spelling"
|
|
122
|
+
)
|
|
123
|
+
)
|
|
124
|
+
self.assertTrue(
|
|
125
|
+
looks_like_standing_rule("never mind that, but always cite the file")
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class TestNudgeAppendedWithoutDisturbingRecall(unittest.TestCase):
|
|
130
|
+
def test_nudge_string_is_the_create_directive_advisory(self):
|
|
131
|
+
self.assertIn("create_directive", _DIRECTIVE_CAPTURE_NUDGE)
|
|
132
|
+
self.assertIn("directive_capture_check", _DIRECTIVE_CAPTURE_NUDGE)
|
|
133
|
+
|
|
134
|
+
def test_nudge_carries_the_dedup_caveat(self):
|
|
135
|
+
# #2903 Fix 6.2: the nudge must tell the model NOT to re-create a rule
|
|
136
|
+
# that already exists as an active directive.
|
|
137
|
+
self.assertIn("already exists", _DIRECTIVE_CAPTURE_NUDGE)
|
|
138
|
+
self.assertIn("active_directives", _DIRECTIVE_CAPTURE_NUDGE)
|
|
139
|
+
|
|
140
|
+
def test_combine_appends_nudge_after_recall_block(self):
|
|
141
|
+
base = "<hindsight_memories>\n…\n</hindsight_memories>"
|
|
142
|
+
out = _combine_context(base, _DIRECTIVE_CAPTURE_NUDGE)
|
|
143
|
+
# Recall block preserved verbatim, nudge appended after it.
|
|
144
|
+
self.assertTrue(out.startswith(base))
|
|
145
|
+
self.assertTrue(out.endswith(_DIRECTIVE_CAPTURE_NUDGE))
|
|
146
|
+
self.assertIn(base + "\n\n", out)
|
|
147
|
+
|
|
148
|
+
def test_combine_with_no_nudge_leaves_recall_output_untouched(self):
|
|
149
|
+
base = "<hindsight_memories>\n…\n</hindsight_memories>"
|
|
150
|
+
self.assertEqual(_combine_context(base, None), base)
|
|
151
|
+
|
|
152
|
+
def test_combine_with_only_nudge_emits_nudge_alone(self):
|
|
153
|
+
self.assertEqual(
|
|
154
|
+
_combine_context("", _DIRECTIVE_CAPTURE_NUDGE), _DIRECTIVE_CAPTURE_NUDGE
|
|
155
|
+
)
|
|
156
|
+
self.assertEqual(
|
|
157
|
+
_combine_context(None, _DIRECTIVE_CAPTURE_NUDGE), _DIRECTIVE_CAPTURE_NUDGE
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
def test_combine_empty_is_empty(self):
|
|
161
|
+
self.assertEqual(_combine_context(None, None), "")
|
|
162
|
+
self.assertEqual(_combine_context("", ""), "")
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class TestTrivialSkipUnaffected(unittest.TestCase):
|
|
166
|
+
"""The nudge must not perturb the trivial-stateless recall gate
|
|
167
|
+
(recall.py:582). Corrections carry standing-rule signals and are never
|
|
168
|
+
stateless, so they must NOT be trivial-skipped; greetings still are."""
|
|
169
|
+
|
|
170
|
+
def test_corrections_are_never_trivial_skipped(self):
|
|
171
|
+
for p in CORRECTIONS:
|
|
172
|
+
with self.subTest(prompt=p):
|
|
173
|
+
self.assertFalse(
|
|
174
|
+
_is_trivial_stateless("", p),
|
|
175
|
+
f"correction {p!r} was trivial-skipped — recall gate regressed",
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
def test_greetings_still_trivial_skip(self):
|
|
179
|
+
for p in ("hi", "hello", "what time is it", "good morning"):
|
|
180
|
+
with self.subTest(prompt=p):
|
|
181
|
+
self.assertTrue(_is_trivial_stateless("", p))
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
if __name__ == "__main__":
|
|
185
|
+
unittest.main()
|