@@ -185,13 +185,115 @@ def _handoff_recorded_identities(handoff: dict[str, object]) -> list[str]:
185185
186186
187187def _verified_handoff_tree (root : Path , handoff : dict [str , object ]) -> str | None :
188+ repositories = handoff .get ("repository" ) if isinstance (handoff .get ("repository" ), list ) else []
189+ for repository in repositories :
190+ if not isinstance (repository , dict ):
191+ continue
192+ recorded_root = str (repository .get ("root" ) or "" ).strip ()
193+ metadata = repository .get ("metadata" ) if isinstance (repository .get ("metadata" ), dict ) else {}
194+ identity = str (metadata .get ("actual_commit" ) or "" ).strip ()
195+ if recorded_root and identity :
196+ tree = _git_tree_id (Path (recorded_root ).expanduser ().resolve (), identity )
197+ if tree :
198+ return tree
188199 for identity in _handoff_recorded_identities (handoff ):
189200 tree = _git_tree_id (root , identity )
190201 if tree :
191202 return tree
192203 return None
193204
194205
206+ def _plan_task_order (args : argparse .Namespace , plan_id : str ) -> dict [str , int ]:
207+ order : dict [str , int ] = {}
208+ for row in index_plans (args ):
209+ if row .get ("type" ) != "task" or row .get ("plan_id" ) != plan_id :
210+ continue
211+ front_matter , _body = read_front_matter (artifact_path_from_row (row , args ))
212+ task_id = str (front_matter .get ("id" ) or row .get ("id" ) or "" )
213+ value = front_matter .get ("order" )
214+ rank : int | None = None
215+ if isinstance (value , int ) and not isinstance (value , bool ):
216+ rank = value
217+ elif isinstance (value , str ) and re .fullmatch (r"[1-9]\d*" , value .strip ()):
218+ rank = int (value )
219+ if task_id and rank is not None :
220+ if rank in order .values ():
221+ raise SystemExit ("acceptance-blocked: final plan task order is ambiguous" )
222+ order [task_id ] = rank
223+ return order
224+
225+
226+ def _material_repository_root (
227+ args : argparse .Namespace ,
228+ plan_id : str ,
229+ validated : list [tuple [dict [str , object ], dict [str , object ]]],
230+ commands : list [str ],
231+ ) -> Path :
232+ entries : list [tuple [Path , str ]] = []
233+ material = [pair for pair in validated if _handoff_has_material_changes (* pair )]
234+ if not material :
235+ return project_root (args )
236+ for handoff , _brief in material :
237+ handoff_has_provenance = False
238+ repositories = handoff .get ("repository" ) if isinstance (handoff .get ("repository" ), list ) else []
239+ for repository in repositories :
240+ if not isinstance (repository , dict ):
241+ continue
242+ recorded = str (repository .get ("root" ) or "" ).strip ()
243+ metadata = repository .get ("metadata" ) if isinstance (repository .get ("metadata" ), dict ) else {}
244+ identity = str (metadata .get ("actual_commit" ) or "" ).strip ()
245+ if recorded and identity :
246+ entries .append ((Path (recorded ).expanduser ().resolve (), identity ))
247+ handoff_has_provenance = True
248+ if not handoff_has_provenance :
249+ try :
250+ fallback = _resolve_final_plan_workspace (args )
251+ except SystemExit as error :
252+ raise SystemExit (
253+ "acceptance-blocked: material handoff repository provenance is unavailable"
254+ ) from error
255+ entries .append ((fallback , "HEAD" ))
256+ roots = {root for root , _identity in entries }
257+ if len (roots ) == 1 :
258+ return next (iter (roots ))
259+ task_order = _plan_task_order (args , plan_id )
260+ material_ranks : list [int ] = []
261+ for handoff , brief in validated :
262+ if not _handoff_has_material_changes (handoff , brief ):
263+ continue
264+ task_id = str (brief .get ("task_id" ) or "" )
265+ if task_id not in task_order :
266+ raise SystemExit ("acceptance-blocked: final plan task order is unavailable" )
267+ material_ranks .append (task_order [task_id ])
268+ terminal_material_rank = max (material_ranks ) if material_ranks else - 1
269+ acceptance_entries : list [tuple [Path , str ]] = []
270+ for handoff , brief in validated :
271+ if not any (_handoff_command_result (handoff , command ) == "passed" for command in commands ):
272+ continue
273+ task_id = str (brief .get ("task_id" ) or "" )
274+ rank = task_order .get (task_id )
275+ if rank is None or rank < terminal_material_rank :
276+ continue
277+ repositories = handoff .get ("repository" ) if isinstance (handoff .get ("repository" ), list ) else []
278+ for repository in repositories :
279+ if not isinstance (repository , dict ):
280+ continue
281+ recorded = str (repository .get ("root" ) or "" ).strip ()
282+ metadata = repository .get ("metadata" ) if isinstance (repository .get ("metadata" ), dict ) else {}
283+ identity = str (metadata .get ("actual_commit" ) or "" ).strip ()
284+ if recorded and identity :
285+ acceptance_entries .append ((Path (recorded ).expanduser ().resolve (), identity ))
286+ fresh_acceptance_roots : set [Path ] = set ()
287+ for root , identity in acceptance_entries :
288+ head_tree = _git_tree_id (root , "HEAD" )
289+ recorded_tree = _git_tree_id (root , identity )
290+ if head_tree is not None and recorded_tree is not None and head_tree == recorded_tree :
291+ fresh_acceptance_roots .add (root )
292+ if len (fresh_acceptance_roots ) == 1 :
293+ return next (iter (fresh_acceptance_roots ))
294+ raise SystemExit ("acceptance-blocked: final plan repository is ambiguous" )
295+
296+
195297def _acceptance_result_detail (results : set [str ]) -> str :
196298 if not results :
197299 return "missing"
@@ -280,7 +382,7 @@ def _assert_archive_plan_acceptance(
280382 commands = _declared_integration_commands (body )
281383 if not commands :
282384 return
283- git_root = project_root (args )
385+ git_root = _material_repository_root (args , plan_id , validated , commands )
284386 terminal_tree = _git_tree_id (git_root , "HEAD" )
285387 material = [pair for pair in validated if _handoff_has_material_changes (* pair )]
286388 for command in commands :
@@ -309,7 +411,7 @@ def _assert_archive_plan_acceptance(
309411 raise SystemExit (
310412 f"acceptance-blocked: declared plan-level acceptance { command } is { _acceptance_result_detail (judged )} "
311413 )
312- workspace = _resolve_final_plan_workspace (args )
414+ workspace = git_root if material else _resolve_final_plan_workspace (args )
313415 for command in commands :
314416 _assert_archive_command_state_neutral (command , workspace )
315417
@@ -457,9 +559,22 @@ def cmd_archive_plan(args: argparse.Namespace) -> None:
457559 replace_front_matter_value (root_path , "status" , "Completed" )
458560 moved .append (move_to_archive (root_path , active_root , archived_root ))
459561
460- active_plan_dir = active_root / args .id
562+ sibling_plan_dir = root_path .with_suffix ("" )
563+ indexed_active_dirs = {
564+ active_root / artifact_path_from_row (row , args ).relative_to (active_root ).parts [0 ]
565+ for row in rows
566+ if row .get ("type" ) == "task"
567+ and row .get ("plan_id" ) == args .id
568+ and is_relative_to (artifact_path_from_row (row , args ), active_root )
569+ }
570+ if sibling_plan_dir .is_dir () and is_relative_to (sibling_plan_dir , active_root ):
571+ active_plan_dir = sibling_plan_dir
572+ elif len (indexed_active_dirs ) == 1 :
573+ active_plan_dir = next (iter (indexed_active_dirs ))
574+ else :
575+ active_plan_dir = active_root / args .id
461576 if active_plan_dir .exists ():
462- archived_plan_dir = archived_root / args . id
577+ archived_plan_dir = archived_root / active_plan_dir . name
463578 if archived_plan_dir .exists ():
464579 raise SystemExit (f"Archived plan directory already exists: { archived_plan_dir } " )
465580 archived_plan_dir .parent .mkdir (parents = True , exist_ok = True )
0 commit comments