From 1901980b22a5b0555dcb31c9c8a8a6a41fb5048f Mon Sep 17 00:00:00 2001 From: Edbert Chan <1916223+EdbertChan@users.noreply.github.com> Date: Fri, 9 Oct 2026 16:04:33 -0700 Subject: [PATCH 1/6] Block a CAT-UNVERIFIED tag that hands a read-only check back as 'say X' The ledger now refuses a reply that tags a claim and offers 'say ...' or 'tell me to test it' for a read-only check. The frustration watchdog accepts 'I am running X now' as a valid next step. Co-Authored-By: Claude Sonnet 5.5 Change-Id: I3b5ef1297fe1f20eec746fd4f74ccc44c54516ca --- .../frustration-watchdog/claude_stop_check.py | 9 +++- .../frustration-watchdog/tests/test_hooks.py | 17 +++++++ engine/hooks/unverified-tag-ledger/detect.py | 47 +++++++++++++++++++ .../unverified-tag-ledger/tests/test_hooks.py | 28 +++++++++++ 4 files changed, 100 insertions(+), 1 deletion(-) diff --git a/engine/hooks/frustration-watchdog/claude_stop_check.py b/engine/hooks/frustration-watchdog/claude_stop_check.py index 8b9e6f513..134b5744f 100755 --- a/engine/hooks/frustration-watchdog/claude_stop_check.py +++ b/engine/hooks/frustration-watchdog/claude_stop_check.py @@ -86,6 +86,11 @@ r"i(?:'ll| will) (?:handle|do|take|run|fix|keep|watch))\b" ) +CHECK_IN_FLIGHT_RE = re.compile( + r"(?i)\bi(?:'m| am) (?:running|checking|testing|probing|reading|querying|fetching) " + r"[^.\n]{1,100}?\bnow\b" +) + HOOK_REFUSAL_RE = re.compile(r"\s*PreToolUse:\S+ hook error\b") """How Claude Code records a tool call a PreToolUse hook refused: an is_error tool_result whose text opens "PreToolUse:Bash hook error: []: ...".""" @@ -209,7 +214,9 @@ def ends_the_wait(message): stripped = message.rstrip() if stripped.endswith("?"): return True # a direct question is a handoff - return bool(NEXT_STEP_RE.search(message) or ETA_RE.search(message)) + return bool( + NEXT_STEP_RE.search(message) or ETA_RE.search(message) or CHECK_IN_FLIGHT_RE.search(message) + ) def detect(data): diff --git a/engine/hooks/frustration-watchdog/tests/test_hooks.py b/engine/hooks/frustration-watchdog/tests/test_hooks.py index cac956e73..79e633d50 100755 --- a/engine/hooks/frustration-watchdog/tests/test_hooks.py +++ b/engine/hooks/frustration-watchdog/tests/test_hooks.py @@ -72,6 +72,23 @@ def test_waiting_user_with_concrete_action_passes(self): finally: os.unlink(path) + def test_check_in_flight_counts_as_next_step(self): + path = transcript_with([("2026-08-18T02:13:30Z", "i am waiting for you to do something")]) + try: + blocked, _ = run_hook(path, "I am running the GraphQL probe now. Result in this turn.") + self.assertFalse(blocked) + finally: + os.unlink(path) + + def test_narrated_past_work_is_not_a_check_in_flight(self): + path = transcript_with([("2026-08-18T02:13:30Z", "i am waiting for you to do something")]) + try: + blocked, err = run_hook(path, "I ran the GraphQL probe earlier and it looked fine.") + self.assertTrue(blocked) + self.assertIn("impatience-shaped", err) + finally: + os.unlink(path) + def test_direct_question_counts_as_handoff(self): path = transcript_with([("2026-08-18T02:13:30Z", "WHY ARE WE NOT LAUNCHING A ZOOM MEETING RIGHT NOW")]) try: diff --git a/engine/hooks/unverified-tag-ledger/detect.py b/engine/hooks/unverified-tag-ledger/detect.py index 475ec645b..d60df8f7c 100644 --- a/engine/hooks/unverified-tag-ledger/detect.py +++ b/engine/hooks/unverified-tag-ledger/detect.py @@ -66,7 +66,20 @@ re.IGNORECASE | re.DOTALL, ) +HANDBACK_RE = re.compile( + r"\b(?:say|reply|type|tell me to)\s+(?:the word\s+)?(?:[\"'\u2018\u201c][^\"'\u2019\u201d]{1,60}[\"'\u2019\u201d]" + r"|(?:go|test|test it|run it|check it|yes|ok|okay|probe it)\b)", + re.IGNORECASE, +) +WRITE_ACTION_RE = re.compile( + r"\b(?:push|deploy|delete|merge|commit|post|send|apply|write|create|publish|" + r"overwrite|force|restart|kill|install|submit|approve|release)\w*\b", + re.IGNORECASE, +) +SENTENCE_SPLIT_RE = re.compile(r"(?<=[.!?])\s+|\n+") + RULE_NO_VERIFICATION_TOOL = "unverified-tag-ledger.no-verification-tool" +RULE_HANDBACK = "unverified-tag-ledger.handback" RULE_DO_NOT_EMIT = "unverified-tag-ledger.do-not-emit" RULE_PROMPT_REMINDER = "unverified-tag-ledger.prompt-reminder" @@ -242,6 +255,22 @@ def emitted_tags(message: str) -> list[str]: return [match.group(0) for match in markers.TAG_RE.finditer(prose)] +def handback_offers(message: str) -> list[str]: + """Sentences outside code that hand a read-only check back to the user. + + "say 'test it'" and "tell me to test it" make the user the trigger for a + check the agent can run itself. An offer whose sentence names a write + (push, deploy, delete, ...) is a real approval gate and is left alone. + """ + prose = INLINE_CODE_RE.sub("", FENCED_RE.sub("", message or "")) + offers = [] + for sentence in SENTENCE_SPLIT_RE.split(prose): + sentence = " ".join(sentence.split()) + if HANDBACK_RE.search(sentence) and not WRITE_ACTION_RE.search(sentence): + offers.append(sentence) + return offers + + def evaluate(payload: dict, mode: str | None = None) -> dict: """Record the turn, then decide whether it may end. @@ -290,6 +319,14 @@ def evaluate(payload: dict, mode: str | None = None) -> dict: if not new_claims: return {"note": "\n".join(notes), "block": ""} + offers = handback_offers(message) + if offers: + return {"note": "", "block": ( + "unverified-tag-ledger: this reply tags a claim as unverified and hands the check back " + f"to the user: {'; '.join(offers[:MAX_LISTED])}. The check is read-only, so the user " + "does not need to trigger it. Run the check now (Bash/Read/Grep/WebFetch) and paste its " + "output. This block is not released by stop_hook_active: deleting the offer always ends the loop.")} + if tools is None: notes.append( f"unverified-tag-ledger: logged {len(new_claims)} CAT-UNVERIFIED claim(s), but this " @@ -339,6 +376,16 @@ def _stop_findings(event: dict[str, object]) -> list[Finding]: if not block: return [] message = _last_assistant_text(payload) + if "hands the check back" in block: + return [ + Finding( + rule_id=RULE_HANDBACK, + subject=offer, + message=block, + evidence=offer, + ) + for offer in handback_offers(message) + ] if behavior == "do_not_emit": tags = emitted_tags(message) return [ diff --git a/engine/hooks/unverified-tag-ledger/tests/test_hooks.py b/engine/hooks/unverified-tag-ledger/tests/test_hooks.py index cfd4633a3..eef7c4dde 100644 --- a/engine/hooks/unverified-tag-ledger/tests/test_hooks.py +++ b/engine/hooks/unverified-tag-ledger/tests/test_hooks.py @@ -179,6 +179,34 @@ def test_rewrite_turn_is_released_so_the_block_cannot_loop(self) -> None: self.payload(REAL_TAG_1, tools=False, stop_hook_active=True)) self.assertEqual(verdict["block"], "") + def test_tag_plus_say_offer_is_blocked_even_after_a_read(self) -> None: + reply = ( + f"{REAL_TAG_1}\n\nIf you want me to settle it, say 'test it' and I will run the probe.") + verdict = self.detect.evaluate(self.payload(reply, tools=True)) + self.assertIn("hands the check back", verdict["block"]) + self.assertIn("run the check now", verdict["block"].lower()) + + def test_tag_plus_tell_me_to_test_offer_is_blocked(self) -> None: + reply = f"{REAL_TAG_1}\n\nTell me to test it and I will." + verdict = self.detect.evaluate(self.payload(reply, tools=True)) + self.assertIn("hands the check back", verdict["block"]) + + def test_say_offer_block_is_not_released_by_stop_hook_active(self) -> None: + reply = f"{REAL_TAG_1}\n\nSay 'go' and I will run it." + verdict = self.detect.evaluate( + self.payload(reply, tools=True, stop_hook_active=True)) + self.assertIn("hands the check back", verdict["block"]) + + def test_tag_plus_say_offer_for_a_write_is_allowed(self) -> None: + reply = f"{REAL_TAG_1}\n\nSay 'push it' and I will push the branch to origin." + verdict = self.detect.evaluate(self.payload(reply, tools=True)) + self.assertEqual(verdict["block"], "") + + def test_say_offer_without_a_tag_is_allowed(self) -> None: + verdict = self.detect.evaluate( + self.payload("Ran the probe: 3 passed. Say 'next' for the follow-up.", tools=True)) + self.assertEqual(verdict["block"], "") + def test_untagged_turn_with_no_tools_is_never_blocked(self) -> None: verdict = self.detect.evaluate( self.payload("Short answer, nothing claimed.", tools=False)) From ade44cb6c7419e60fb3be832cbcca94e1174742d Mon Sep 17 00:00:00 2001 From: Edbert Chan <1916223+EdbertChan@users.noreply.github.com> Date: Mon, 12 Oct 2026 21:44:14 -0700 Subject: [PATCH 2/6] learned: list the user's own items before keyword search when asked who made their thing Asked which merge request produced a page the user owns, the assistant ran keyword searches, hit timeouts and unrelated results, and reported none found. Listing the user's own merge requests found it at once. One working-style rule line. Co-Authored-By: Claude Sonnet 5.5 Change-Id: Icf84fb21574c8af5632bafe3e0b894131970e558 --- corpus/CLAUDE.learned.md | 1 + 1 file changed, 1 insertion(+) diff --git a/corpus/CLAUDE.learned.md b/corpus/CLAUDE.learned.md index fb09cdd3a..72c5ea4f2 100644 --- a/corpus/CLAUDE.learned.md +++ b/corpus/CLAUDE.learned.md @@ -49,3 +49,4 @@ Engine-only install drops this file; reflect Accepted global rules land here. - When finishing a merge or rebase, reconcile three things before calling it resolved: what each side declared (its commit messages and PR description: the intent, and the tests it names), what each side's code actually does, and what the resolved result does. All three must agree: every declared intent is still visible in `git diff -- `, and every test either side named still passes on the result. A merge with no textual conflict can still break one side's declared behavior, so a clean merge is checked the same way. Never take one side's whole file (`git checkout --theirs` / `--ours `, or copying the file from one branch) unless the other side's changes to that file are provably empty; taking a whole side silently reverts what the other branch added, including hunks that never conflicted (git-checkout(1), `--ours`/`--theirs`: https://git-scm.com/docs/git-checkout#Documentation/git-checkout.txt---ours). When a declaration and its code disagree, or the two sides' intents contradict, stop and flag it to me instead of choosing: run `why` on the conflicting lines (`git log -L` or `git log -S` plus each commit's PR body) and apply `principle-prove-it`, so the flag names the two commits or PRs, quotes the declared intent next to the code, and pastes the failing test output. Brun, Holmes, Ernst and Notkin, "Proactive Detection of Collaboration Conflicts" (ESEC/FSE 2011, https://doi.org/10.1145/2025113.2025139), across Git, Perl5 and Voldemort, found "that 33% of merges that were reported to contain no textual conflicts by the VCS in fact contained higher-order conflicts" (build or test failures), so a clean textual merge is not evidence the two intentions still hold. - An item I approved as "fix it properly later" stays in every status update and every final summary, marked open, until it is done or I drop it. A recap that lists only what finished makes the deferred half disappear, and the next session never learns it was owed. No known prior art. - Before spending more than a couple of tool calls on an infrastructure, security, or "does X exist" question, search saved memory for the topic and for its reverse framing: the fix as well as the symptom, "X is off" as well as "X is missing". An answer already written down costs one read; rediscovering it costs the whole investigation. No known prior art. +- When I ask what made, wrote, or owns something I say is mine (a page, a file, a job, a merge request), list my own items first — everything I created or authored, newest first — and match against that list before any keyword search over everyone's items. A keyword search finds only the words the author chose; an author filter finds the item whatever it is called, and my own list is short enough to read in full. No known prior art. From 680674c3077b77f784d7bd90a4bdfcb96694bbf1 Mon Sep 17 00:00:00 2001 From: Edbert Chan <1916223+EdbertChan@users.noreply.github.com> Date: Mon, 12 Oct 2026 21:38:47 -0700 Subject: [PATCH 3/6] Make the pre-push check run on the shell macOS ships The pre-push review-unit check used a bash 4 builtin that the default macOS shell (3.2) does not have, so every push from a Mac stopped at the hook. Read the NUL-separated file lists with a portable loop instead. Co-Authored-By: Claude Sonnet 5.5 Change-Id: I186973c821653410e5d48a13d1acd2f2e4bb8433 --- scripts/git-hooks/pre-push | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/git-hooks/pre-push b/scripts/git-hooks/pre-push index 61b583dae..34516a867 100755 --- a/scripts/git-hooks/pre-push +++ b/scripts/git-hooks/pre-push @@ -157,7 +157,7 @@ while read -r local_ref local_sha remote_ref remote_sha; do echo "pre-push: UNCHECKED: git diff $diff_from $local_sha failed for $local_ref; $bypass" >&2 exit 1 fi - mapfile -d '' files <"$names" + files=(); while IFS= read -r -d '' _path; do files+=("$_path"); done <"$names" if [[ ${#files[@]} -eq 0 ]]; then continue fi @@ -173,7 +173,7 @@ while read -r local_ref local_sha remote_ref remote_sha; do if old_diff_from=$(git merge-base "$base_ref" "$remote_sha" 2>/dev/null); then old_names=$(mktemp) if git diff --name-only -z "$old_diff_from" "$remote_sha" >"$old_names"; then - mapfile -d '' old_files <"$old_names" + old_files=(); while IFS= read -r -d '' _path; do old_files+=("$_path"); done <"$old_names" if [[ ${#old_files[@]} -eq 0 ]]; then old_rc=0 else @@ -182,7 +182,7 @@ while read -r local_ref local_sha remote_ref remote_sha; do if [[ $old_rc -eq 1 ]]; then delta_names=$(mktemp) if git diff --name-only -z "$remote_sha" "$local_sha" >"$delta_names"; then - mapfile -d '' delta_files <"$delta_names" + delta_files=(); while IFS= read -r -d '' _path; do delta_files+=("$_path"); done <"$delta_names" if [[ ${#delta_files[@]} -eq 0 ]]; then continue fi From ae9861b827fe1b5f6dafebe4d69ded967dea9d3b Mon Sep 17 00:00:00 2001 From: Edbert Chan <1916223+EdbertChan@users.noreply.github.com> Date: Fri, 9 Oct 2026 16:13:52 -0700 Subject: [PATCH 4/6] cat-mode: local-only repos end in a local merge, never GitHub Co-Authored-By: Claude Sonnet 5.5 Change-Id: I26c788b179793511a9191bede9861bd8521bc347 --- corpus/skills/cat-mode/SKILL.md | 3 ++- .../cat-mode/tests/fires_local_only_no_github.md | 2 ++ .../tests/stays_silent_remote_repo_publishes.md | 2 ++ corpus/skills/cat-mode/tests/test_artifact_sweeps.py | 10 ++++++++++ 4 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 corpus/skills/cat-mode/tests/fires_local_only_no_github.md create mode 100644 corpus/skills/cat-mode/tests/stays_silent_remote_repo_publishes.md diff --git a/corpus/skills/cat-mode/SKILL.md b/corpus/skills/cat-mode/SKILL.md index 3464274d4..35b869244 100644 --- a/corpus/skills/cat-mode/SKILL.md +++ b/corpus/skills/cat-mode/SKILL.md @@ -22,7 +22,8 @@ Once direction is set, act — don't ask permission for each sub-step. One fully - **Under an active `/loop`-style standing directive, don't end a report with "want me to continue?"** A trailing question is a permission request. Treat the next obvious step as already authorized; report what you found AND what you're doing next. Ask only for a destructive/production action or a real fork with no default. - **Keep named follow-ups attached to durable/background execution until the directive is complete. Completion includes every invoked skill's required landing phase.** Waiting on CI, a queue, or a subagent means sleep/wake with a clock-time ETA stated to the user, never a poll loop; on wake, resume without restatement. Arming a watcher and yielding is not waiting. -- **Commit, push, and open the PR automatically once the change is verified — don't wait to be asked.** The user says "commit and push" and "make a pr for this" / "make a pr stack" as trailing commands; treat publication as the standing default after shippable work, not a separate ask. Follow the installed `make-pr` / `draft-pr` skill (Invoker: Mergify stack via `safe-stack-push` when that repo uses stacks). "Land" means through to landed: fix CI, resolve conflicts, and deploy once merged. Still stop for: user said not to PR, plan-only / no code yet, unfinished todos they are mid-driving, or a dirty tree that mixes unrelated work that needs an explicit split first. Absent "land," deploys and other production-visible actions still get asked first. +- **Commit, push, and open the PR automatically once the change is verified — don't wait to be asked.** The user says "commit and push" and "make a pr for this" / "make a pr stack" as trailing commands; treat publication as the standing default after shippable work, not a separate ask. Follow the installed `make-pr` / `draft-pr` skill (Invoker: Mergify stack via `safe-stack-push` when that repo uses stacks). "Land" means through to landed: fix CI, resolve conflicts, and deploy once merged. Still stop for: user said not to PR, the repo is local-only (see the next bullet), plan-only / no code yet, unfinished todos they are mid-driving, or a dirty tree that mixes unrelated work that needs an explicit split first. Absent "land," deploys and other production-visible actions still get asked first. +- **A local-only repo ends in a local branch and a local merge, never GitHub.** When the repo has no GitHub remote, or the user says local or don't use GitHub, every apply, PR, or plan flow ends in a local branch and a local merge. Never push, run `gh`, or probe GitHub credentials. Ask once if unsure. Point Invoker's `repoUrl` at the repo's local folder and finish with a local merge, not a pull request. - **A named multi-step done-gate stays locked until receipt.** When the user names a close-out (for example full backfill plus PR), finish with a receipt before opening new product scope or plan attachments. - **Do not race publication against a live long job in the same thread.** Run commit-scoped preflight before drafting the PR body; finish or park the backfill ownership hand-off first. - **Cursor `/pr-skill` is not Invoker's merge-gate.** Invoker merge-gate and PR-split sessions publish via installed `invoker-make-pr` + create-pr.mjs / `gh api`; implement/slice tasks do not publish PRs. Do not diagnose `__merge__` / merge-clone sessions as "/pr-skill didn't fire" — the always-on `/pr-skill` rule is scoped to Cursor chat, so a merge-clone session was never inside it and its silence is expected. diff --git a/corpus/skills/cat-mode/tests/fires_local_only_no_github.md b/corpus/skills/cat-mode/tests/fires_local_only_no_github.md new file mode 100644 index 000000000..6bda9d6d8 --- /dev/null +++ b/corpus/skills/cat-mode/tests/fires_local_only_no_github.md @@ -0,0 +1,2 @@ +User: "Apply this plan. The repo is local, don't use GitHub." +Assistant: I work on a local branch, point Invoker at the repo's local folder, finish with a local merge, and run no push, no gh, and no credential probe. diff --git a/corpus/skills/cat-mode/tests/stays_silent_remote_repo_publishes.md b/corpus/skills/cat-mode/tests/stays_silent_remote_repo_publishes.md new file mode 100644 index 000000000..490f77bd2 --- /dev/null +++ b/corpus/skills/cat-mode/tests/stays_silent_remote_repo_publishes.md @@ -0,0 +1,2 @@ +User: "Verified. Ship it." (repo has a GitHub remote, nobody said local) +Assistant: I git push the branch and open the PR with make-pr, as the standing default. diff --git a/corpus/skills/cat-mode/tests/test_artifact_sweeps.py b/corpus/skills/cat-mode/tests/test_artifact_sweeps.py index 3ef67b1fd..75d2925b7 100644 --- a/corpus/skills/cat-mode/tests/test_artifact_sweeps.py +++ b/corpus/skills/cat-mode/tests/test_artifact_sweeps.py @@ -22,6 +22,16 @@ def test_correction_surface_rule_has_trigger_fixture(self): for required in ("raw data", "aggregate table", "chart source", "live Sheet read-back"): self.assertIn(required, fixture) + def test_local_only_rule_has_fixtures(self): + skill = (SKILL_DIR / "SKILL.md").read_text(encoding="utf-8") + fires = (SKILL_DIR / "tests" / "fires_local_only_no_github.md").read_text(encoding="utf-8") + silent = (SKILL_DIR / "tests" / "stays_silent_remote_repo_publishes.md").read_text(encoding="utf-8") + + self.assertIn("A local-only repo ends in a local branch and a local merge", skill) + self.assertIn("local merge", fires) + self.assertNotIn("git push", fires.split("Assistant:")[1]) + self.assertIn("git push", silent) + if __name__ == "__main__": unittest.main() From df8f867340fd875be63f9ac0a3cb7588319293cb Mon Sep 17 00:00:00 2001 From: CI Bot Date: Tue, 13 Oct 2026 06:05:52 +0000 Subject: [PATCH 5/6] cat-mode: avoid backticked gh reference --- corpus/skills/cat-mode/SKILL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/corpus/skills/cat-mode/SKILL.md b/corpus/skills/cat-mode/SKILL.md index 35b869244..3c924ed27 100644 --- a/corpus/skills/cat-mode/SKILL.md +++ b/corpus/skills/cat-mode/SKILL.md @@ -23,7 +23,7 @@ Once direction is set, act — don't ask permission for each sub-step. One fully - **Under an active `/loop`-style standing directive, don't end a report with "want me to continue?"** A trailing question is a permission request. Treat the next obvious step as already authorized; report what you found AND what you're doing next. Ask only for a destructive/production action or a real fork with no default. - **Keep named follow-ups attached to durable/background execution until the directive is complete. Completion includes every invoked skill's required landing phase.** Waiting on CI, a queue, or a subagent means sleep/wake with a clock-time ETA stated to the user, never a poll loop; on wake, resume without restatement. Arming a watcher and yielding is not waiting. - **Commit, push, and open the PR automatically once the change is verified — don't wait to be asked.** The user says "commit and push" and "make a pr for this" / "make a pr stack" as trailing commands; treat publication as the standing default after shippable work, not a separate ask. Follow the installed `make-pr` / `draft-pr` skill (Invoker: Mergify stack via `safe-stack-push` when that repo uses stacks). "Land" means through to landed: fix CI, resolve conflicts, and deploy once merged. Still stop for: user said not to PR, the repo is local-only (see the next bullet), plan-only / no code yet, unfinished todos they are mid-driving, or a dirty tree that mixes unrelated work that needs an explicit split first. Absent "land," deploys and other production-visible actions still get asked first. -- **A local-only repo ends in a local branch and a local merge, never GitHub.** When the repo has no GitHub remote, or the user says local or don't use GitHub, every apply, PR, or plan flow ends in a local branch and a local merge. Never push, run `gh`, or probe GitHub credentials. Ask once if unsure. Point Invoker's `repoUrl` at the repo's local folder and finish with a local merge, not a pull request. +- **A local-only repo ends in a local branch and a local merge, never GitHub.** When the repo has no GitHub remote, or the user says local or don't use GitHub, every apply, PR, or plan flow ends in a local branch and a local merge. Never push, run the GitHub CLI, or probe GitHub credentials. Ask once if unsure. Point Invoker's `repoUrl` at the repo's local folder and finish with a local merge, not a pull request. - **A named multi-step done-gate stays locked until receipt.** When the user names a close-out (for example full backfill plus PR), finish with a receipt before opening new product scope or plan attachments. - **Do not race publication against a live long job in the same thread.** Run commit-scoped preflight before drafting the PR body; finish or park the backfill ownership hand-off first. - **Cursor `/pr-skill` is not Invoker's merge-gate.** Invoker merge-gate and PR-split sessions publish via installed `invoker-make-pr` + create-pr.mjs / `gh api`; implement/slice tasks do not publish PRs. Do not diagnose `__merge__` / merge-clone sessions as "/pr-skill didn't fire" — the always-on `/pr-skill` rule is scoped to Cursor chat, so a merge-clone session was never inside it and its silence is expected. From c79e01980b803505a20717af27d4dbaca303d98a Mon Sep 17 00:00:00 2001 From: CI Bot Date: Tue, 13 Oct 2026 06:06:11 +0000 Subject: [PATCH 6/6] =?UTF-8?q?invoker:=20wf-1791870257543-107/repair=20?= =?UTF-8?q?=E2=80=94=20Repair=20PR=20#1378:=20failed=5Fchecks:=20test;?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Exit code: 0