@@ -64,6 +64,8 @@ def fake_run_agent(cli_ref, prompt, **kwargs):
6464 assert not (output_dir / "deliberation.json" ).exists ()
6565 plan = json .loads ((output_dir / "plan.json" ).read_text ())
6666 assert plan .get ("mode" ) is None
67+ run_meta = json .loads ((output_dir / "run.json" ).read_text ())
68+ assert "deliberation" not in run_meta
6769
6870
6971def test_cli_passes_deliberation_flag_to_aboyeur (tmp_path , monkeypatch ):
@@ -110,6 +112,55 @@ def test_mark_duplicate_scopes_flags_later_entries():
110112 assert marked [2 ].status == "valid"
111113
112114
115+ def test_perspective_prompts_embed_distinct_recorded_scopes (monkeypatch , tmp_path ):
116+ scopes = [
117+ _scope ("graphtrail-context" , "migration" ),
118+ _scope ("graphtrail-callers" , "migrate.run" ),
119+ ]
120+ monkeypatch .setattr (deliberation , "derive_evidence_scopes" , lambda cwd , task , count = 3 : scopes )
121+ plan = deliberation .build_plan (_roster (), "choose migration" , cwd = tmp_path )
122+ roster = _roster ()
123+ prompt_by_worker : dict [str , str ] = {}
124+ for lens in plan .lenses :
125+ if lens .role != "perspective" :
126+ continue
127+ prompt_by_worker [lens .worker ] = deliberation .build_worker_prompt (
128+ roster .agents [lens .worker ],
129+ Assignment (worker = lens .worker , task = lens .task , stage = lens .stage ),
130+ plan = plan ,
131+ prior_results = None ,
132+ read_only = False ,
133+ )
134+ assert len (prompt_by_worker ) == 2
135+ assert len (set (prompt_by_worker .values ())) == 2
136+ for lens in plan .lenses :
137+ if lens .role != "perspective" :
138+ continue
139+ prompt = prompt_by_worker [lens .worker ]
140+ assert lens .scope .text in prompt
141+ for other in plan .lenses :
142+ if other .role != "perspective" or other .worker == lens .worker or not other .scope .text :
143+ continue
144+ assert other .scope .text not in prompt
145+
146+
147+ def test_plan_payload_lists_distinct_evidence_scopes (monkeypatch , tmp_path ):
148+ scopes = [
149+ _scope ("graphtrail-context" , "migration" ),
150+ _scope ("graphtrail-callers" , "migrate.run" ),
151+ ]
152+ monkeypatch .setattr (deliberation , "derive_evidence_scopes" , lambda cwd , task , count = 3 : scopes )
153+ plan = deliberation .build_plan (_roster (), "choose migration" , cwd = tmp_path )
154+ payload = deliberation .plan_payload (plan )
155+ assert payload ["mode" ] == "deliberation"
156+ evidence_scopes = payload ["evidence_scopes" ]
157+ fingerprints = {(item ["kind" ], item ["reference" ]) for item in evidence_scopes }
158+ assert len (fingerprints ) == len (evidence_scopes ) == 2
159+ assigned_stage_one = {assignment .worker for assignment in plan .assignments if assignment .stage == 1 }
160+ invalid_workers = {lens .worker for lens in plan .invalid_lenses }
161+ assert not assigned_stage_one & invalid_workers
162+
163+
113164def test_build_plan_records_invalid_role_label_lens (monkeypatch , tmp_path ):
114165 scopes = [
115166 _scope ("graphtrail-context" , "task-a" ),
@@ -336,6 +387,129 @@ def test_assemble_artifact_marks_unavailable_challenger(monkeypatch, tmp_path):
336387 deliberation .validate_schema (artifact )
337388
338389
390+ def test_stage_one_dispatch_prompts_exclude_prior_perspectives (monkeypatch , tmp_path ):
391+ scopes = [
392+ _scope ("graphtrail-context" , "migration" ),
393+ _scope ("graphtrail-callers" , "migrate.run" ),
394+ ]
395+ monkeypatch .setattr (deliberation , "derive_evidence_scopes" , lambda cwd , task , count = 3 : scopes )
396+ plan = deliberation .build_plan (_roster (), "choose migration strategy" , cwd = tmp_path )
397+ captured : list [str ] = []
398+
399+ def fake_run_agent (cli_ref , prompt , ** kwargs ):
400+ captured .append (prompt )
401+ return agents .AgentResult (
402+ text = json .dumps (
403+ {
404+ "position" : "ok" ,
405+ "assumptions" : [],
406+ "evidence_references" : [],
407+ "agreements" : [],
408+ "conflicts" : [],
409+ }
410+ ),
411+ ok = True ,
412+ )
413+
414+ monkeypatch .setattr (agents , "run_agent" , fake_run_agent )
415+ aboyeur .dispatch (
416+ list (plan .assignments ),
417+ _roster (),
418+ build_prompt = deliberation .make_prompt_builder (plan ),
419+ cwd = tmp_path ,
420+ )
421+ stage_one = [prompt for prompt in captured if "Independent perspectives" not in prompt ]
422+ stage_two = [prompt for prompt in captured if "Independent perspectives" in prompt ]
423+ assert len (stage_one ) == 2
424+ assert len (stage_two ) == 1
425+
426+
427+ def test_synthesis_context_preserves_dissenting_perspective_positions ():
428+ artifact = {
429+ "recommendation" : "pick redis" ,
430+ "confidence" : "medium" ,
431+ "minority_report" : "sessions remain safer for rollback" ,
432+ "unresolved_conflicts" : [],
433+ "perspectives" : [
434+ {
435+ "worker" : "coder" ,
436+ "position" : "use redis" ,
437+ "evidence_scope" : {"kind" : "graphtrail-context" },
438+ },
439+ {
440+ "worker" : "reviewer" ,
441+ "position" : "use sessions" ,
442+ "evidence_scope" : {"kind" : "graphtrail-callers" },
443+ },
444+ ],
445+ "challenger" : {"worker" : "analyst" , "minority_report" : "sessions remain safer for rollback" },
446+ }
447+ context = deliberation .synthesis_context (artifact )
448+ assert "use redis" in context
449+ assert "use sessions" in context
450+ assert "sessions remain safer for rollback" in context
451+
452+
453+ def test_deliberation_run_leaves_outcome_ledger_unchanged (monkeypatch , tmp_path ):
454+ output_dir = tmp_path / "run"
455+ memory_dir = tmp_path / "memory" / "outcome"
456+ memory_dir .mkdir (parents = True )
457+ records_path = memory_dir / "records.jsonl"
458+ records_path .write_text ('{"artifact_id":"existing"}\n ' , encoding = "utf-8" )
459+ scopes = [
460+ _scope ("graphtrail-context" , "migration" ),
461+ _scope ("graphtrail-callers" , "migrate.run" ),
462+ ]
463+ monkeypatch .setattr (deliberation , "derive_evidence_scopes" , lambda cwd , task , count = 3 : scopes )
464+ monkeypatch .setattr (
465+ aboyeur ,
466+ "dispatch" ,
467+ lambda assignments , roster , ** kwargs : [
468+ WorkerResult (
469+ worker = assignment .worker ,
470+ task = assignment .task ,
471+ text = json .dumps (
472+ {
473+ "position" : f"{ assignment .worker } position" ,
474+ "assumptions" : [],
475+ "evidence_references" : [],
476+ "agreements" : [],
477+ "conflicts" : [],
478+ }
479+ if assignment .stage == 1
480+ else {
481+ "attacks" : [],
482+ "minority_report" : "minority" ,
483+ "recommendation" : "ship" ,
484+ "confidence" : "medium" ,
485+ "unresolved_conflicts" : [],
486+ "agreements" : [],
487+ }
488+ ),
489+ ok = True ,
490+ )
491+ for assignment in assignments
492+ ],
493+ )
494+ monkeypatch .setattr (
495+ aboyeur ,
496+ "_run_orchestrator" ,
497+ lambda roster , prompt , ** kwargs : agents .AgentResult (text = "final" , ok = True ),
498+ )
499+
500+ rc = aboyeur .run (
501+ "choose migration strategy" ,
502+ _roster (),
503+ output_dir = output_dir ,
504+ cwd = tmp_path ,
505+ code_graph_enabled = False ,
506+ route_enabled = False ,
507+ deliberation = True ,
508+ )
509+ assert rc == 0
510+ assert records_path .read_text (encoding = "utf-8" ) == '{"artifact_id":"existing"}\n '
511+
512+
339513def test_deliberation_dispatch_passes_prior_results_to_challenger (monkeypatch , tmp_path ):
340514 scopes = [
341515 _scope ("graphtrail-context" , "migration" ),
0 commit comments