@@ -557,6 +557,141 @@ def test_mixed_bash_code_and_handoff_write_requires_new_verification(tmp_path: P
557557 assert blocked ["decision" ] == "block"
558558
559559
560+ def test_stop_ignores_postcapture_brigade_run_artifact_writes (tmp_path : Path , monkeypatch ):
561+ """Issue #483: detached brigade run artifacts under .brigade/runs/ must not re-arm closeout."""
562+ target = _wired_claude (tmp_path )
563+ session_id = "brigade-runs-artifact"
564+ monkeypatch .setattr (runtime , "_run_brief" , lambda repo : "brief" )
565+ runtime .handle_payload ("SessionStart" , _payload (target , "SessionStart" , session_id = session_id ))
566+ runtime .handle_payload (
567+ "PostToolUse" ,
568+ _payload (
569+ target ,
570+ "PostToolUse" ,
571+ session_id = session_id ,
572+ tool_name = "Write" ,
573+ tool_input = {"file_path" : str (target / "file.py" )},
574+ ),
575+ )
576+ state = runtime .read_session_state (target , session_id )
577+ run_dir = target / ".brigade" / "work" / "verify-runs" / "run-1"
578+ run_dir .mkdir (parents = True )
579+ (run_dir / "receipt.json" ).write_text (
580+ json .dumps (
581+ {
582+ "run_id" : "run-1" ,
583+ "status" : "completed" ,
584+ "started_at" : state ["last_verification_write_at" ],
585+ "harness_session" : {
586+ "harness" : "claude" ,
587+ "fingerprint" : state ["session_fingerprint" ],
588+ },
589+ }
590+ )
591+ + "\n "
592+ )
593+ artifact = target / ".brigade" / "runs" / "detached-1" / "run.json"
594+ runtime .handle_payload (
595+ "PostToolUse" ,
596+ _payload (
597+ target ,
598+ "PostToolUse" ,
599+ session_id = session_id ,
600+ tool_name = "Bash" ,
601+ tool_input = {"command" : f"mkdir -p { artifact .parent } && touch { artifact } " },
602+ ),
603+ )
604+
605+ updated = runtime .read_session_state (target , session_id )
606+ assert updated ["last_verification_write_at" ] == state ["last_verification_write_at" ]
607+ result = runtime .handle_payload ("Stop" , _payload (target , "Stop" , session_id = session_id , stop_hook_active = False ))
608+ assert "decision" not in result
609+
610+
611+ def test_stop_still_blocks_after_source_edit_post_capture (tmp_path : Path , monkeypatch ):
612+ """Issue #483: real source edits after capture must still trip the closeout gate."""
613+ target = _wired_claude (tmp_path )
614+ session_id = "source-edit-post-capture"
615+ monkeypatch .setattr (runtime , "_run_brief" , lambda repo : "brief" )
616+ runtime .handle_payload ("SessionStart" , _payload (target , "SessionStart" , session_id = session_id ))
617+ runtime .handle_payload (
618+ "PostToolUse" ,
619+ _payload (
620+ target ,
621+ "PostToolUse" ,
622+ session_id = session_id ,
623+ tool_name = "Write" ,
624+ tool_input = {"file_path" : str (target / "file.py" )},
625+ ),
626+ )
627+ state = runtime .read_session_state (target , session_id )
628+ run_dir = target / ".brigade" / "work" / "verify-runs" / "run-1"
629+ run_dir .mkdir (parents = True )
630+ (run_dir / "receipt.json" ).write_text (
631+ json .dumps (
632+ {
633+ "run_id" : "run-1" ,
634+ "status" : "completed" ,
635+ "started_at" : state ["last_verification_write_at" ],
636+ "harness_session" : {
637+ "harness" : "claude" ,
638+ "fingerprint" : state ["session_fingerprint" ],
639+ },
640+ }
641+ )
642+ + "\n "
643+ )
644+ runtime .handle_payload (
645+ "PostToolUse" ,
646+ _payload (
647+ target ,
648+ "PostToolUse" ,
649+ session_id = session_id ,
650+ tool_name = "Write" ,
651+ tool_input = {"file_path" : str (target / "src.py" ), "content" : "changed\n " },
652+ ),
653+ )
654+
655+ updated = runtime .read_session_state (target , session_id )
656+ assert updated ["last_verification_write_at" ] > state ["last_verification_write_at" ]
657+ blocked = runtime .handle_payload ("Stop" , _payload (target , "Stop" , session_id = session_id , stop_hook_active = False ))
658+ assert blocked ["decision" ] == "block"
659+
660+
661+ def test_posttooluse_brigade_run_does_not_record_write (tmp_path : Path , monkeypatch ):
662+ """Issue #483: brigade run bash must not bump verification write timestamps."""
663+ target = _wired_claude (tmp_path )
664+ session_id = "brigade-run-bash"
665+ monkeypatch .setattr (runtime , "_run_brief" , lambda repo : "brief" )
666+ runtime .handle_payload ("SessionStart" , _payload (target , "SessionStart" , session_id = session_id ))
667+ runtime .handle_payload (
668+ "PostToolUse" ,
669+ _payload (
670+ target ,
671+ "PostToolUse" ,
672+ session_id = session_id ,
673+ tool_name = "Write" ,
674+ tool_input = {"file_path" : str (target / "file.py" )},
675+ ),
676+ )
677+ state = runtime .read_session_state (target , session_id )
678+ pretool = _payload (
679+ target ,
680+ "PreToolUse" ,
681+ session_id = session_id ,
682+ tool_name = "Bash" ,
683+ tool_input = {"command" : "brigade run --detach -- echo noop" },
684+ )
685+ assert runtime .handle_payload ("PreToolUse" , pretool ) is None
686+ assert "pending_bash_fingerprint" in runtime .read_session_state (target , session_id )
687+
688+ runtime .handle_payload ("PostToolUse" , {** pretool , "hook_event_name" : "PostToolUse" })
689+
690+ updated = runtime .read_session_state (target , session_id )
691+ assert updated ["last_verification_write_at" ] == state ["last_verification_write_at" ]
692+ assert "pending_bash_fingerprint" not in updated
693+
694+
560695def test_repo_worktree_fingerprint_detects_dirty_tracked_same_size_rewrite (tmp_path : Path ):
561696 target = _git_wired_claude (tmp_path )
562697 tracked = target / "tracked.txt"
0 commit comments