@@ -35,6 +35,93 @@ def audit(
3535 output .error (str (e ))
3636 raise typer .Exit (code = 1 )
3737
38+ # Surface passive worktree issues (passive_drift, passive_fetch_failed,
39+ # passive_dirty_worktree). Iterates state.tasks; for each (task, repo)
40+ # in task.passive_repos, audits the worktree and merges issues into
41+ # the matching RepoAudit entry in `report`.
42+ try :
43+ from mship .core .repo_state import (
44+ audit_passive_worktrees , RepoAudit , AuditReport , Issue ,
45+ )
46+ state = container .state_manager ().load ()
47+
48+ # Group passive worktrees by task. For each task, run a separate
49+ # audit so messages can include the task slug for disambiguation.
50+ passive_per_task : list [tuple [str , dict , dict , dict ]] = []
51+ for task_slug , task in state .tasks .items ():
52+ if not task .passive_repos :
53+ continue
54+ paths : dict = {}
55+ refs : dict = {}
56+ canonicals : dict = {}
57+ for repo_name in task .passive_repos :
58+ if repo_name not in config .repos :
59+ continue
60+ rc = config .repos [repo_name ]
61+ ref = getattr (rc , "expected_branch" , None ) or getattr (rc , "base_branch" , None )
62+ if ref is None :
63+ continue
64+ wt = task .worktrees .get (repo_name )
65+ if wt is None :
66+ continue
67+ paths [repo_name ] = wt
68+ refs [repo_name ] = ref
69+ canonicals [repo_name ] = rc .path
70+ if paths :
71+ passive_per_task .append ((task_slug , paths , refs , canonicals ))
72+
73+ extra_issues_per_repo : dict [str , list ] = {}
74+ for task_slug , paths , refs , canonicals in passive_per_task :
75+ per_repo = audit_passive_worktrees (paths , refs , canonicals )
76+ for repo_name , issues in per_repo .items ():
77+ if not issues :
78+ continue
79+ # Tag each issue with the task slug for disambiguation
80+ tagged = [
81+ Issue (i .code , i .severity ,
82+ f"[task { task_slug } ] { i .message } " )
83+ for i in issues
84+ ]
85+ extra_issues_per_repo .setdefault (repo_name , []).extend (tagged )
86+
87+ if extra_issues_per_repo :
88+ # Rebuild report.repos so RepoAudit instances containing passive
89+ # repos pick up the merged issues.
90+ new_repos = []
91+ seen : set [str ] = set ()
92+ for r in report .repos :
93+ seen .add (r .name )
94+ extra = extra_issues_per_repo .get (r .name , [])
95+ if extra :
96+ new_repos .append (RepoAudit (
97+ name = r .name ,
98+ path = r .path ,
99+ current_branch = r .current_branch ,
100+ issues = tuple (list (r .issues ) + extra ),
101+ ))
102+ else :
103+ new_repos .append (r )
104+ # Repos with passive issues but not in the audit_repos report
105+ # (e.g. excluded by --repos filter). Append fresh entries.
106+ for repo_name , extra in extra_issues_per_repo .items ():
107+ if repo_name in seen :
108+ continue
109+ rc = config .repos .get (repo_name )
110+ if rc is None :
111+ continue
112+ new_repos .append (RepoAudit (
113+ name = repo_name ,
114+ path = rc .path ,
115+ current_branch = None ,
116+ issues = tuple (extra ),
117+ ))
118+ report = AuditReport (repos = tuple (new_repos ))
119+ except Exception as e :
120+ # Best-effort: failing to audit passive worktrees should not break
121+ # the canonical audit output.
122+ if not json_output :
123+ output .warning (f"passive audit unavailable: { e } " )
124+
38125 if json_output :
39126 import json as _json
40127 print (_json .dumps (report .to_json (workspace = config .workspace ), indent = 2 ))
0 commit comments