Skip to content

fix(pixel): a refused rm -rf keeps a current passing test, and the owner gets a receipt instead of guard text - #6753

Open
Lightheartdevs wants to merge 14 commits into
mainfrom
claude/pixel-deletion-refusal-keeps-verified-work
Open

Lightheartdevs wants to merge 14 commits into
mainfrom
claude/pixel-deletion-refusal-keeps-verified-work

Conversation

@Lightheartdevs

@Lightheartdevs Lightheartdevs commented Sep 26, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

Open prompt 07 (photo-renamer) on main d4a61f33, tower2, Qwen3-Coder-Next. This is the same hardware and model as the Hermes and OpenCode controls, which both delivered in about 20 s. Pixel took 25 s and was graded PARTIAL, with product outcome failed.

Evidence: runs/tower2-main-d4a61f33-sweep1/07-photo-renamer on tower2, session fe9ee859-c533-483c-b978-5c4a672dc126, session file sha256 8d21ebc1d247f332de279b80e3fd205d5a477a15211dd5bb1f47f158e4ca6f89.

The same prompt on tower3 (Qwen3.5-27B, session 047d3145, sha256 e861b9f0…) was also graded PARTIAL for a related guard reason: python3 -m unittest reported "Ran 11 tests … OK" with exit 0, but the program under a negative test then printed Error: Folder '/nonexistent/folder/path' does not exist., and the unittest recognizer counted that line as a failure. #6752 fixes that, and this branch now contains #6752 (see "Unittest verdict" below).

Change

After a refused recursive deletion (recursiveDeleteRefusalVerification, tool-loop-guard.mjs), the owner gets a receipt built from host state instead of the model-directed text. The run keeps passed only when the receipt can stand for the whole result:

  • The owner asked for a change to files and for nothing to be said in the reply, and no research was requested or web results used. The answer detector is deliberately broad; a wrong match only keeps the run failed, as on main. It matches a question, and any ask for an explanation, description, summary, overview, report or results, usage or instructions, how it works or how to run it, documentation, a walkthrough, a comparison or citations; "tell me", "show me", "give me", "let me know"; "include …"; or a reply, answer or response. "Can you write …?" is a request, not a question.
  • A write, edit or apply_patch succeeded in this run.
  • The latest recognized test passed and at least one test it reported passed: a run where every test was skipped, an expected failure, deselected or todo (unittest, also colored; pytest; Jest/Vitest; node:test; cargo; go) does not count.
  • That test ran in a directory that contains a file this run changed. The workspace root and the Playground folder, which hold other projects, cover only their own top-level files. A command that names a parent (..), home (~) or absolute path outside its directory covers nothing.
  • That pass is current: no call that could change the workspace was admitted after it; no sibling call from the same model response and no other background command ran while it ran or ended after it; and none is still running.
  • The rest of the task evaluation passed without a publication receipt, and the run is not Operations, exact-download, preview-required, managed-extension or managed-team work.

Every other case stays failed, with the same receipt.

Currency

  • agent_end runs endPreviewRevalidation (which advances the preview generation) before the ingress reads delivery. Ending the attempt changes no workspace file, so a pass that was current stays current. Without this, the passed branch was unreachable in production and the receipt wrongly called the pass "not current".
  • A background test is dated from when it started, not from the read-only poll that completed it. A background command that ends after a pass makes that pass stale. A background command that was running when the test started, started while a background test ran, or was observed to end while it ran, marks the pass as overlapped.
  • Sibling calls: OpenClaw runs every before_tool_call of one model response, then the calls together, then each after_tool_call. Each admitted call that could change the workspace is tracked from before_tool_call to after_tool_call; a test that shared that window with another such call, in either order, is overlapped. A read-only sibling, a sibling this guard refused, and a Tool Search call's own nested child do not count. A call without a record counts as overlapped.
  • Refused calls never advance the generation. After a deletion refusal, a refused call is not recorded as a test result.

The receipt

  • It says the command was refused and did not run. It never claims nothing was deleted and contains no instructions aimed at the model.
  • It lists files written and files changed with edit or apply_patch (at most 20). Each path is capped at 200 characters, keeping its start and file name, so the receipt stays far below the 32 KiB ingress bound. The coding-exhausted receipt shares this helper and is otherwise byte-identical.
  • It names the latest test command and the directory it ran in, from the call the model made (before execution control wraps it). A leading cd /workspace/... wins over workdir. Command and directory are capped at 160 characters with backticks and control characters removed.
  • It gives that test's state: passed and current; passed but not current; passed while "another tool call or command ran at the same time"; exited successfully with no passing test; failed; not finished; or none ran.

Progress stop. The budget is checked before the deletion check, so a deletion refusal always comes first; the latch's own refusals are what exhaust the budget afterwards. The deletion receipt now takes precedence over RUN_PROGRESS_STOP_REASON in that case. Loop bounds, signal and abort counts are unchanged.

Unittest verdict (tower3). This branch builds on #6752 (merged in at bf7708967 and at #6752's current head 1363e768c in bf279cc18) and uses #6752's runner-summary verdict. This branch's own earlier recognizer change (eb55782c) was dropped in that merge. The tower3 replay below passes through #6752's verdict.

Disclosure. The workspace guide gains one constant sentence, RECURSIVE_DELETE_CONTRACT: "Do not delete directories recursively (rm -rf or equivalents) unless the owner asked; ODS refuses that and may stop tool use for the turn. Keep temporary test inputs inside the project folder and leave cleanup to the owner." Prompts that change or test named existing code (for example "Fix the bug in Playground/x/y.py, then run the tests") get that sentence alone when the guide is absent. Other turns, including non-coding ones, are unchanged. The guide's length bound in agent-skills.test.mjs moves from 3200 to 3400 characters (now 3364).

Unchanged: the model-facing RECURSIVE_DELETE_REQUIRES_OWNER_REASON, the latch, execControl.signal, abortRun, the strict-stop synthesis skip, beforeAgentFinalize, continuationAllowed (false), and pixel_ingress.mjs. No model text is accepted after a refusal. Nothing outside the deletion-refusal delivery reads the overlap, no-passing-test or coverage state.

Delivered for the recorded tower2 run (status passed):

Pixel stopped using tools because a command included a recursive deletion that you did not ask for. That command was refused and did not run.
Results recorded by Pixel's tools before that:
- File written: `/workspace/Playground/photo-renamer/rename_photos.py`.
- File written: `/workspace/Playground/photo-renamer/test_rename.py`.
- The latest recognized test command, `python3 test_rename.py` in `/workspace/Playground/photo-renamer`, passed, and no tool call that could change the workspace ran after it.
This does not establish complete test coverage or completion of every requested step.

Review follow-up

Commits eb55782c, 65f55390 and d291bb05 address both reviews.

Finding Resolution
passed unreachable in production: agent_end advanced the generation before delivery Fixed: a current pass stays current through agent_end
Receipt said "a later tool call could have changed the workspace" when none ran Fixed by the same change
Tests skipped the real agent_end order Fixed: every run in deletion_refusal_delivery.test.mjs goes through the lifecycle callbacks plugin/index.js registers, and the test pins that agent_end order
Lifted to passed with no work, or for a non-coding request (probes C, D) Fixed: requires a successful write/edit/patch, a change request and no requested written answer; team workers are excluded too
Background test dated from the poll; background command ending after the pass (A, B) Fixed
Skipped-only tests lifted to passed (E, F) Fixed on this path. The normal delivery path still treats skipped-only runs as passing; changing that affects every run and is left for a separate change
Research + script request lifted to passed (Q) Fixed: research requested, citations asked, or web results used keep failed
edit/apply_patch changes missing; directory from workdir over a leading cd (G, S) Fixed
Receipt length unbounded (R: 74,280 characters) Fixed: the same input now gives 4,850 characters
Disclosure missed existing-code fix prompts Fixed for prompts that name workspace code with a change or test verb
Progress stop replaced the receipt Fixed
Only 1 of 5 undelivered photo-renamer hosts reached tower3's recognizer false negative is fixed by #6752, which this branch contains. The rest are not caused by these paths: strixy has genuinely failing tests and hit the coding-loop limit, mac-mini hit path corrections then the progress stop, and the laptop "code in chat only" grade is a harness trace failure (WSL 0x8007274c) on a session that did use tools
Merge-order hazard with #6722 Cannot be removed from this side; the exact replacement assertion is below and was run

Also fixed, from the probes: a pass in a directory that holds none of this run's changes (probe H) no longer completes the run.

Re-review follow-up (bf7708967, 2e9cf8986, 0b8659f90, bf279cc18, c091bd635)

The re-review's finding: a refused deletion must not lift a stale or unrelated passing test to passed.

Finding Resolution
The answer-request gate missed eight phrasings ("provide a brief explanation of how it works", "give me a short summary", "report which photos it renamed", "let me know the results", "include usage instructions in your reply", "give an overview", "tell me how to run it", "describe the edge cases") 2e9cf8986: the broad detector above; those eight and 23 more reply-content tails stay failed; three plain change requests still pass
A background command that ran while the passing test ran, ended after it, or was killed after it (N01-N03) 2e9cf8986: overlapped or stale, not lifted; the receipt says why
No test passed although nothing was "skipped": Python 3.14 colored OK (skipped=1), pytest xfailed/deselected only, Jest todo, node:test todo (N04-N06) 2e9cf8986: reuses #6752's color stripping and summary parsing; the receipt says no test passed
A test run from the workspace root, or one that names .., ~ or an outside absolute path, "covered" a Playground project 2e9cf8986: covers nothing, or only top-level files at the root
Sibling calls: for [write rename_photos.py, python3 -m unittest -v] in one model response, the write's receipt arrives first, so no call was admitted after the test's receipt. The pass lifted to passed with "no tool call that could change the workspace ran after it", although the test ran at the same time as the write. A background test started beside a write lifted the same way. 0b8659f90: sibling calls that could change the workspace mark each other overlapped, in either order; failed with "another tool call or command ran at the same time"
cd /workspace/Playground && python3 -m pytest old-project ran another project's tests, and the Playground folder "contained" this run's files, so it lifted 0b8659f90: the Playground folder covers only its own top-level files, like the root. A run from there that names this run's project is refused too (a false failure, never a false pass)
Branch + main failed 1 test: #6751 (now on main) prefixes the Playground path correction with "Not written: …", and the tower2 replay pinned the recorded text exactly c091bd635: the replay checks that call 1 is refused with a text that contains the recorded sentence; that text is another guard's
#6752's verdict changes Merged: bf7708967 (c0a83a78e) and bf279cc18 (1363e768c)

Tests

tests/deletion_refusal_delivery.test.mjs (26 tests) runs every scenario through the plugin/index.js lifecycle callbacks (before_tool_call, after_tool_call, agent_end) around the real guard.

  • tower2 replay (fixture photo-renamer-tower2-d4a61f33.json, all 11 recorded calls): blocks and prepare() calls match the recording, signal and abort fire once, and delivery after the real agent_end is passed with the receipt above. replyPayloadSending replaces both the empty final and a model claim. The loopback ingress delivers it with pixel_outcome: passed.
  • tower2 counterfactuals: no Rename to Android Framework — update all references #11 gives the same receipt; a write between Add supervisor pattern, background GPU automation, production guardia… #9 and Add PHILOSOPHY.md and reframe README as methodology #10, the failing Improve repo discoverability: tagline, component table, Quick Start p… #5, and no test run all stay failed; a refused rerun of the test and a refused rm -rf … && unittest keep passed.
  • Review probes: A and B (background), C and D (no change, questions) plus a polite request that stays passed, Q (research, citations, web results), E and F (unittest, pytest -q, pytest, and a partial skip that still counts), G (edit and apply_patch listed as "File changed"), H (other directory), S (cd over workdir), team builder, Operations, the progress stop, and R (23 files at depth 30 plus a hostile directory).
  • Re-review probes: N01-N03 (background overlap, with the receipt text and a control that ended before the test), N04-N06 (colored and no-passing-test outputs, with controls where something passed), N20-N27 and 23 reply-content tails (with three controls), coverage (.., --start-directory=.., outside absolute, ~, -t .., root, Playground folder, with in-project controls), and sibling calls: [write, test] with the exact receipt, [test, write], a background test started beside a write; controls [read, test], [test, refused rm -rf] and a Tool Search call with its nested child hooks stay passed.
  • tower3 replay (new fixture photo-renamer-tower3-d4a61f33.json, all 8 recorded calls from session 047d3145): status passed, and the model's own answer is delivered unchanged. Ad-hoc scripts printing Error: or fail: without a unittest verdict still fail, and unittest's own FAIL: block after an OK still fails.
  • Disclosure: the photo-renamer turn has the guide and the sentence once; three existing-code prompts get only the sentence; five other prompts (factual, Operations, explain-a-file, research, "do not edit") get neither. Checked at context budgets 65536 and 16384.
  • tool_loop_guard.test.mjs: three expected-text constants change from "No file was written." to "No file was written or changed with Pixel's file tools."

Red checks (this head's deletion_refusal_delivery.test.mjs on earlier plugins):

  • on 2e9cf8986's plugin: 3 of 26 fail (sibling calls, the Playground folder, and the overlapped wording);
  • on d291bb05's plugin: 6 of 26 fail (the above, the answer-request gate, and the two no-passing-test tests).

Verification

Full Linux plugin suite (remote-js-test.sh, tower2):

Tree Tests Pass Fail
main d4a61f33 (earlier run) 3138 3100 0
d291bb05 3161 3123 0
red check: d291bb05's tests with the plugin from 378bf54e 3161 3104 19
2e9cf8986 3253 3215 0
bf279cc18 3255 3217 0
bf279cc18 + main 8c589ef5 3328 3289 1 (the #6751 wording, fixed in c091bd635)
this head c091bd635 3255 3217 0
c091bd635 + main 8c589ef5 (local merge) 3328 3290 0
c091bd635 + #6722 (ebe90936) 3336 3287 11 (all in #6722's own delivered-text test, as before)

The 19 red-check failures of the first review are 16 of the 21 delivery tests then plus the 3 wording changes in tool_loop_guard.test.mjs.

Recorded-session replays (reviewer 2's replay-multi harness, which ends each run with the real agent_end handler):

  • Earlier, at d291bb05: all 60 open-prompt sessions from the d4a61f3 sweep (6 hosts, sha-verified): only tower2/07 and tower3/07 change, both from failed to passed; the other 58 deliver byte-identical text on main and the branch. 57 qualification-round sessions (rounds 100–107, 312 runs): byte-identical to main.
  • This head: the same harness over the same 117 sessions (60 open-prompt, 57 qualification; 372 runs, 3,728 calls). Against fix(pixel): judge an exit-0 unittest run by the runner's own summary; a coding stop takes precedence over test-command coaching #6752's head 1363e768c: only tower2/07 differs, failed with the model-directed refusal to passed with the receipt above. Against d291bb05: 3 sessions differ, all from fix(pixel): judge an exit-0 unittest run by the runner's own summary; a coding stop takes precedence over test-command coaching #6752's coding-stop order and identical on 1363e768c: strixy/07 and two laptop qualification coding runs (rounds 100 and 107) get one more abortRun at the coding stop; their delivered status stays failed, and in the two laptop runs the stop text changes. tower3/07 is passed on both.
  • Reviewers' probe files (at d291bb05): A, B, C, D, E, F, H, M, P and Q are failed; G, K, L, S and N are passed; R gives a 4,850-character receipt. Probe N (abort not acknowledged, 12 calls after the refusal) gives the same refusal sequence and the same 11 signals and 11 aborts on main, 378bf54e and the branch.

Composition

#6752. This branch contains #6752's head 1363e768c (normal merge commits, no rebase). Merge #6752 first; this branch then adds only its own commits.

#6722. Merges cleanly. Its test "a second recursive deletion or a substitute after the guidance stops the turn" (1 test plus 10 subtests) asserts the old delivered text. Whichever PR merges second changes that assertion to the following; the status stays failed:

status: "failed", text: "Pixel stopped using tools because a command included a recursive deletion that you did not ask for. " +
  "That command was refused and did not run.\nResults recorded by Pixel's tools before that:\n" +
  "- No file was written or changed with Pixel's file tools.\n- No recognized test command ran.\n" +
  "This request is not complete. Ask Pixel to continue, or say explicitly if you want a folder deleted.",

This branch's own tests refuse top-level folders, so they pass unchanged under #6722's guided in-project refusal.

#6702. Merges cleanly with this head (git merge-tree). The disclosure sits in the workspace-guide slot, which #6702 moves into per-turn guidance, so it stays out of the byte-stable system prompt there.

Not done here

  • Fleet rerun. Open prompt 07 has not been rerun on tower2 or tower3 with this branch; that needs the branch deployed, and fleet hosts were read-only for this work. Expected: tower2 DELIVERED when the model repeats the cleanup, and tower3 DELIVERED.
  • Skipped-only runs on the normal path still count as passing (see the table above).
  • Background commands from an earlier turn of the same conversation are not tracked by this run; a pass is judged against this run's own calls.
  • A test in the project directory can still test something else there (for example an old test module). The receipt says "This does not establish complete test coverage".

🤖 Generated with Claude Code

Lightheartdevs and others added 4 commits September 25, 2026 22:40
… a coding stop takes precedence over test-command coaching

Fleet, open-prompt 07 photo-renamer on main d4a61f3.

tower3 (Qwen3.5-27B, PARTIAL): `python3 -m unittest test_photo_renamer -v`
exited 0 with "Ran 11 tests in 0.010s / OK", but a passing negative-path
test had printed "Error: Folder '/nonexistent/folder/path' does not exist."
The exit-0 heuristics in execResultHasNonCleanUnittestOutcome matched that
program output, so the verification stayed failed, the failed-attempt count
grew instead of resetting, and the model's accurate 973-character answer
was replaced by VERIFICATION_FAILED_DELIVERY_PREFIX.

When a result contains unittest's own "Ran N tests in X.XXXs" summary, it
is now judged by the runner's markers only: a zero-test summary or NO TESTS
RAN, a summary not followed by OK, a FAILED ( line, nonzero expected
failures or unexpected successes (also in -v form), or an exact
"FAIL|ERROR: test (module.Class...)" header. Program output such as
"Error:", "ERROR:root:" logging, -v docstrings or AssertionError text no
longer fails a clean run. ANSI colors (Python 3.14 on a pty) are ignored
for this check. Without a summary (custom runners) the existing heuristics
are unchanged, and a nonzero exit still decides first.

strixy (Qwen3.6-35B-A3B): after the coding stop, the model's
`...; echo "EXIT:$?"` reached the non-auditable refusal before the coding
terminal and was told to "Run it directly", contradicting "Do not call
another tool". After a coding stop that refusal now defers to the stop's
terminal handling (terminal block, then abort). Its text, free-correction
accounting and stale-pass invalidation are unchanged.

Tests replay the recorded tower3, strixy (tail) and tower2 sessions call by
call through createToolLoopGuard, reproducing every recorded refusal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ner gets a receipt instead of guard text

tower2 open-prompt 07 photo-renamer (Qwen3-Coder-Next, main d4a61f3,
session fe9ee859): rename_photos.py and test_rename.py were written and
`python3 test_rename.py` passed. An unrequested "manual test" then ended
`&& rm -rf /workspace/test_manual`. The guard refused it before it ran,
refused the next exec and aborted the run, as designed, but
taskVerificationForRun returned {failed, RECURSIVE_DELETE_REQUIRES_OWNER_REASON}
before looking at the pass. The owner got only the 270-character
model-directed text ("Do not retry ... wait for a new owner instruction"),
with no mention of the files or the passing test. Hermes and OpenCode
delivered the same prompt on the same hardware and model.

- taskVerificationForRun: the body after the deletion check moves into
  workspaceTaskVerification (a pure refactor for runs without a refusal).
  After a refusal, the result is 'passed' only when that evaluation passed
  without a publication receipt, the latest recognized test passed, its
  pass generation is still current (refused calls never advance it), no
  exec session is pending, and the run is not Operations, exact-download,
  preview, managed extension or team work. Every other case is 'failed'.
- Both cases deliver an owner receipt built from host state: the refusal
  (the command was refused and did not run), the files written (same
  filter and 20-file cap as the coding-exhausted receipt, now shared), the
  latest recognized test command and directory from its fingerprint
  (bounded, no backticks or control characters) with its state, and for
  'failed' that the request is incomplete. It never claims nothing was
  deleted and has no model-directed imperatives. The model-facing
  blockReason is unchanged.
- afterToolCall: after a deletion refusal, a call the guard refused ran
  nothing, so its blocked receipt no longer records a test result. A
  refused rerun of the test (or `rm -rf x && python3 -m unittest`) could
  otherwise mark the real pass failed and name a test that never ran.
- The latch, signal, abort, strict-stop synthesis skip, finalization and
  continuation exclusions are unchanged; no model text is accepted after
  a refusal (reply_payload_sending replaces it with the receipt).
- The workspace guide discloses the tripwire in one constant sentence
  (RECURSIVE_DELETE_CONTRACT); the guide's length bound moves from 3200
  to 3400 characters, as it did for the named-item sentence. The
  September 16 and compact contract pins are untouched.

Tests replay all 11 recorded calls through createToolLoopGuard with a stub
execControl (fixture from the recorded session, sha256 8d21ebc1...) plus
counterfactuals (no later call, a later write, refusal after the failing
test, no test run, refused test rerun), a pending test, Operations,
exact-download and preview runs, the receipt bounds, the workspace guide,
and the ingress delivering the receipt with pixel_outcome passed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n case

The refused `rm -rf ... && python3 -m unittest -v` case deleted an
in-project folder. Open #6722 turns an in-project deletion into a guided,
non-terminal refusal, so on that composition the case no longer exercised
the terminal refusal it tests. Target /workspace/test_manual, which stays
terminal with or without #6722 (as the recorded call did).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…; echoed summaries, compaction and fixture names

Review of #6752 (462c784):

- A clean "Ran N tests ... OK" summary made every other line program output,
  so a check that runs beside unittest in the same exec lost the df3f4bf
  protection: a module-level print checker imported by discovery, ad-hoc
  checks after unittest.main(exit=False), a test that catches its own
  assertion and prints the traceback. With a summary present, a line-start
  FAIL or AssertionError (optionally indented) now fails the result, as does
  unittest's failure-count line in any case ("failed (failures=1)").
- The failure-header check matched program output such as "ERROR:
  IMG_0002.jpg (corrupt)". A header now counts only right after unittest's
  "=" * 70 separator and only with a test name and a dotted identifier.
- andChainVerificationParams accepts literal `echo` setup before the test
  command, and the echoed words share the result, so they could print a
  summary the test never produced. A chain with an echo setup segment is
  judged by the text heuristics only, on the exec and process-poll paths.
- Tool Search compaction replaced the result with the summary alone, so a
  result that passed despite program output hid that output from the model.
  Compaction again requires the text heuristics to find nothing, as on
  d4a61f3.
- Fixtures are renamed unittest-verdict-*-d4a61f33.json; #6753 adds a
  different photo-renamer-tower2-d4a61f33.json. The tower2 replay stops at
  the custom-runner call 8 and no longer asserts the rm -rf delivery text
  that #6753 changes. Replays pin the refusal text only for the calls this
  change concerns and otherwise check that the recorded path is followed, so
  #6751's Playground wording does not break them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Lightheartdevs and others added 6 commits September 25, 2026 23:41
… whole, current result

Review of #6753 found the 'passed' branch unreachable in production and too
weak where it did apply. This addresses every confirmed finding.

Currency:
- agent_end (endPreviewRevalidation) advanced the generation before the
  ingress read delivery, so a current pass always looked stale and the
  tower2 run still delivered 'failed'. Ending the attempt changes no
  workspace file, so a pass that was current stays current.
- A background test is dated from its start, not from the poll that
  completed it, and a background command that ends after a pass makes the
  pass stale (a read-only poll never advanced the generation).

Eligibility ('passed' also requires):
- a write, edit or patch succeeded in this run, and the owner asked for a
  change and for no written answer (no question, explanation, summary or
  citation), and no research was requested or web results used;
- the pass did not skip every test (unittest, pytest, Jest/Vitest,
  node:test, cargo, go summaries) and ran in a directory that contains a
  file this run changed;
- the run is not a managed team worker (the coordinator was already out).
- A progress stop caused by the latch's own refusals no longer replaces
  the deletion receipt: the budget is checked before the deletion, so the
  refusal always came first.

Receipt accuracy and bounds:
- edit and apply_patch changes are listed as "File changed";
- the test command and directory come from the call the model made, and a
  leading cd /workspace/... wins over the workdir;
- each listed path is capped at 200 characters (start and file name kept),
  so 20 lines stay far below the 32 KiB ingress bound; this also bounds the
  coding-exhausted receipt, which is otherwise byte-identical.

tower3 photo-renamer (same prompt, Qwen3.5-27B): a clean unittest verdict
("Ran 11 tests ... OK", exit 0) was marked failed because the program under
a negative test printed "Error: Folder ... does not exist.". With that
verdict present, only unittest's own upper-case FAIL/ERROR/FAILED lines
count; ad-hoc scripts without the verdict keep the case-insensitive check.

Disclosure: prompts that change or test named existing code (for example
"Fix the bug in Playground/x/y.py, then run the tests") now get the
deletion sentence when the workspace guide is absent; other turns are
unchanged.

Tests: deletion_refusal_delivery now drives every run through the
lifecycle callbacks plugin/index.js registers (real agent_end order), adds
the reviewers' probes A-H, Q, R, S, the team worker, the progress stop and
a recorded tower3 replay (fixture from session 047d3145, sha256 e861b9f0).
Replaying all 60 d4a61f3 open-prompt sessions changes only tower2/07 and
tower3/07 (both now passed); 57 qualification sessions (312 runs) are
byte-identical to main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…mposes with #6722

Open #6722 guides a first recursive deletion inside a project (such as its
__pycache__) instead of stopping the turn, so this case now targets
/workspace/test_manual. The no-change assertion is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e slot

Same prompt text as before. Leaving the appendSystemContext template and
the agent-skills import line untouched keeps this change clear of open
#6702, which moves that template into per-turn guidance; the disclosure
then travels with the rest of the per-message guidance.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… checker fail lines and AssertionError fail it

Re-review of #6752 (probes R01-R06, R28, R30 and two real Python repros):

- In summary mode, AssertionError anywhere in the result (any case) and a
  line that starts with FAIL in any case before ":", "(", a space, a tab or
  the line end now fail the run, as in the reviewer's prototype. A checker's
  "fail: ...", "Fail: ...", "Test failed: AssertionError('...')" or
  "x add(-1, -2): AssertionError: ..." beside a clean summary is failed
  again. A passing test that prints AssertionError text is now failed too:
  a false failure, never a false pass.
- The summary is trusted only when the command the model sent runs
  `python -m unittest` directly (alone or after `cd /workspace/...`). Any
  and-chain setup segment (echo, cp <file> /dev/stdout, mv, mkdir, touch,
  true) and any direct test script (`python3 test_x.py`, whose own code runs
  around unittest.main()) keep the text heuristics. This closes the cp and
  echo summary forging, a script that prints a summary of its own, and
  "Error:"/"ERROR:" checks after unittest.main(exit=False).
- withoutTerminalColors is factored out for reuse.

Replays: tower3, strixy and tower2 are unchanged. Over the 63 recorded
fleet sessions (3,350 calls) this commit differs from 462c784 nowhere.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ude/pixel-deletion-refusal-keeps-verified-work

#6753 now builds on #6752's unittest verdict instead of its own. The
conflict in execResultHasNonCleanUnittestOutcome is resolved as in the
re-review's union: #6752's runner-summary verdict (runner-only trust,
checker FAIL lines and AssertionError) plus #6753's execResultTexts and
skipped-only check. #6753's own verdict change from eb55782 (a
case-sensitive failure match whenever a clean "Ran N tests ... OK" appears
anywhere) is dropped; without a trusted summary the text heuristics are
main's, byte for byte.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…request that asks for nothing in the reply and a test nothing overlapped

Re-review of #6753 (probes N01, N04-N06, N20-N25):

- Answer-request gate (medium). The receipt stands for the result only when
  the owner asked for nothing to be said in the reply. The detector is now
  deliberately broad: a question, or any ask for an explanation, description,
  summary, overview, report or results, usage or instructions, how it works
  or how to run it, documentation, a walkthrough, a comparison or citations,
  "tell me", "show me", "give me", "let me know", "include ...", or a reply,
  answer or response. A match keeps the run failed with the same honest
  receipt, as for any other incomplete run. All eight phrasings from the
  re-review now stay failed.
- Overlapping background command (low-medium). A background exec that was
  running when the test started, started while a background test ran, or was
  observed to end while it ran, marks that pass as overlapped; so does any
  background exec still pending when a foreground test passes. An overlapped
  pass is not lifted, and the receipt says why. A background command that
  ended before the test started does not block the lift.
- No passing test (low). The check reuses #6752's withoutTerminalColors and
  its unittest summary/outcome parsing: Python 3.14 colored
  "OK (skipped=N)" with N >= Ran, skipped plus expected failures covering
  every test, and any pytest summary line without a nonzero "passed" (skipped,
  xfailed, xpassed or deselected only) keep the refusal failed; Jest/Vitest
  "Tests" lines and node:test "todo" are covered the same way. The receipt
  line now says no test passed instead of claiming every test was skipped.
- Coverage (extra, conservative). A test run from the workspace root covers
  only top-level files, and a test command that names a parent, home or
  absolute path outside its directory covers nothing.

Unchanged and re-verified: the loop bound after the refusal, the deletion
decisions (no new deletion allowed), the receipt text for a current pass,
and the capped long-path receipt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Lightheartdevs and others added 4 commits September 26, 2026 04:50
…ocess beside the runner

verificationCommandIsAuditable refuses pipes, ";", redirections and and/or
chains, but not a lone "&". `python3 -m unittest test_calc & python3
check_calc.py` is therefore a plain, auditable unittest command, and
c0a83a7 trusted its runner summary. The checker's "Error: add(-1, -2)
returned 1, expected -3" next to that clean summary passed; d4a61f3 fails
the same result by its text heuristics.

A command with "&" (other than the trailing stderr merge) is now judged by
the text heuristics, on the exec and the process-poll paths. The
pre-execution gate is unchanged.

Red check: the new test fails on c0a83a7's guard (88/89) and the whole
file fails 21 of 89 on 11c8f71's guard.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…call or from the Playground folder is not lifted

Two stale or unrelated passes still lifted a refused deletion to passed on
2e9cf89:

- Sibling calls. OpenClaw runs every before_tool_call of one model response,
  then the calls together, then each after_tool_call. For [write
  rename_photos.py, python3 -m unittest -v] the write's receipt arrives
  first, so no call was admitted after the test's receipt, and the pass was
  "current" although the test ran at the same time as the write. The owner
  got status passed and "no tool call that could change the workspace ran
  after it". The same held for a background test started beside the write.
  Each admitted call that could change the workspace is now tracked from
  before_tool_call to after_tool_call; a test that shared that window with
  another such call, in either order, is overlapped and not lifted. A
  read-only sibling, a sibling this guard refused, and a Tool Search call's
  own nested child do not count. A call without a record counts as
  overlapped.
- The Playground folder. `cd /workspace/Playground && python3 -m pytest
  old-project` ran another project's tests, and the folder "contained" this
  run's Playground/photo-renamer files, so it lifted. The Playground folder
  now covers only its own top-level files, like the workspace root.

The overlapped receipt line now reads "another tool call or command ran at
the same time", which is accurate for both background commands and
siblings. Nothing outside the deletion-refusal delivery reads these states.

Red check: the updated deletion_refusal_delivery.test.mjs fails 3 of 26 on
2e9cf89's plugin (the sibling test, the Playground cases, and the
overlapped wording) and 6 of 26 on d291bb0's plugin.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… into claude/pixel-deletion-refusal-keeps-verified-work

Brings in #6752's latest verdict fix: a unittest summary is not trusted when
& starts a second process beside the runner.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tion so the replay composes with #6751

#6751 (now on main) prefixes the Playground routing correction with "Not
written: project files go in a descriptive Playground folder. Call write
again now with path ...". The tower2 replay asserted the recorded text
exactly, so the full suite on this branch merged with main (0a63258)
failed that one test (3328 tests, 1 fail). The replay now checks that call 1
is refused with a text that contains the recorded sentence; that text is
another guard's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant