File size: 8,233 Bytes
b67a1e9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
#!/usr/bin/env python3
import json, re
from pathlib import Path
from collections import defaultdict

ROOT = Path(__file__).resolve().parents[1]
INV = ROOT / 'inventory'
E = INV / 'evidence'
T = ROOT / 'triage'
T.mkdir(parents=True, exist_ok=True)

CATEGORIES = ['KEEP','MERGE','PAUSE','ARCHIVE','URGENT','TOKEN_BURN_CANDIDATE','HUMAN_DECISION_REQUIRED']

def load_json(name, default):
    p = E / name
    if not p.exists(): return default
    return json.loads(p.read_text(encoding='utf-8'))

def load_projects():
    rows=[]
    p=INV/'projects.jsonl'
    for line in p.read_text(encoding='utf-8').splitlines():
        if line.strip():
            try: rows.append(json.loads(line))
            except Exception: pass
    seen={}; out=[]
    for r in rows:
        path=r.get('path') or ''
        if not path or path in seen: continue
        seen[path]=1; out.append(r)
    return out

def norm_path(p): return str(p or '')
def basename(path): return Path(path).name or path.rstrip('/').split('/')[-1]
def is_cache(path):
    s=path.lower()
    return any(x in s for x in ['/repo_cache/','/eval/fixtures/','/codex-skill-validation-fixtures/','/node_modules/','.venv'])
def theme_map(theme_rows):
    m=defaultdict(set)
    for row in theme_rows:
        m[row.get('repo','')].update(row.get('themes',[]))
    return m

def main():
    projects=load_projects()
    uncomm={x['repo']:x for x in load_json('uncommitted_changes.json',[])}
    branches={x['repo']:x for x in load_json('open_branches.json',[])}
    broken={x['repo']:x for x in load_json('broken_projects.json',[])}
    dups=load_json('duplicate_projects.json',[])
    themes=theme_map(load_json('theme_clusters.json',[]))
    dup_paths={p:row for row in dups for p in row.get('repos',[])}

    rows=[]
    buckets={c:[] for c in CATEGORIES}
    for p in projects:
        path=norm_path(p.get('path'))
        name=p.get('name') or basename(path)
        score=int(p.get('score') or 0)
        todo=int(p.get('todo_fixme_count') or 0)
        plan=int(p.get('plan_hit_count') or 0)
        agent=int(p.get('agent_artifact_count') or 0)
        readme=bool(p.get('readme_path') or p.get('readme_summary'))
        dirty=uncomm.get(path,{}).get('count',0) or (p.get('git_status') or {}).get('dirty_tracked_count',0) or 0
        branch_count=len(branches.get(path,{}).get('branches',[]))
        is_dup=path in dup_paths
        th=themes.get(path,set())
        tech=','.join(p.get('technologies',[]) or p.get('project_type',[]) or ['unknown'])
        reasons=[]
        cats=[]

        # Priority rules: categories are non-exclusive.
        if dirty >= 10 or (dirty >= 5 and th) or path in ['/home/smlflg/.hermes','/home/smlflg/.claude','/home/smlflg/.codex']:
            cats.append('URGENT'); reasons.append(f'uncommitted_changes={dirty}')
        if dirty or branch_count > 1 or p.get('risk_level') == 'high' or path in broken:
            cats.append('HUMAN_DECISION_REQUIRED'); reasons.append('state/risk needs owner decision')
        if is_dup:
            cats.append('MERGE'); reasons.append('duplicate cluster')
        if is_cache(path) or re.search(r'(-old|backup|copy|repo_cache|fixture|astropy__|django__|sympy__)', path, re.I):
            cats.append('ARCHIVE'); reasons.append('cache/fixture/old-artifact signal')
        # token burn: many plans/todos/agent artifacts but weak closure/readme or duplicated meta system.
        if (plan >= 5 and todo >= 20) or (agent >= 5 and dirty >= 5) or (is_dup and score >= 50):
            cats.append('TOKEN_BURN_CANDIDATE'); reasons.append(f'plan={plan}, todo={todo}, agent_artifacts={agent}, score={score}')
        keep_signal = score >= 65 or bool(th & {'agent_control_tower_near','hermes_near','uni_near'}) or readme and score >= 50
        if keep_signal and 'ARCHIVE' not in cats:
            cats.append('KEEP'); reasons.append('high score/theme/readme signal')
        if not cats or ('KEEP' not in cats and 'MERGE' not in cats and 'ARCHIVE' not in cats and score < 50):
            cats.append('PAUSE'); reasons.append('low confidence or not top-level valuable now')
        # If KEEP plus MERGE, the repo is valuable but should be merged/consolidated, not developed separately.
        categories=list(dict.fromkeys(cats))
        row={
            'name': name, 'path': path, 'categories': categories,
            'score': score, 'dirty_count': dirty, 'branch_count': branch_count,
            'todo_fixme_count': todo, 'plan_hit_count': plan, 'agent_artifact_count': agent,
            'themes': sorted(th), 'duplicate_cluster': dup_paths.get(path,{}).get('key'),
            'readme_present': readme, 'tech': tech, 'reasons': reasons[:8]
        }
        rows.append(row)
        for c in categories: buckets[c].append(row)

    # Sort within buckets: urgent by dirty, keep by score, archive by cache-ish path, pause low score.
    for c in CATEGORIES:
        buckets[c].sort(key=lambda r: (-(r['dirty_count'] if c=='URGENT' else r['score']), r['path']))

    (T/'PROJECT_PRIORITIZATION.json').write_text(json.dumps({'categories':buckets,'projects':rows},indent=2,ensure_ascii=False),encoding='utf-8')

    md=['# Project Prioritization','','Categories are non-exclusive. KEEP+MERGE means valuable but consolidate instead of building separately. URGENT/HUMAN_DECISION means do not delegate blindly.','']
    for c in CATEGORIES:
        md.append(f'## {c} ({len(buckets[c])})')
        for r in buckets[c][:80]:
            extra=[]
            if r['dirty_count']: extra.append(f"dirty={r['dirty_count']}")
            if r['branch_count']>1: extra.append(f"branches={r['branch_count']}")
            if r['duplicate_cluster']: extra.append(f"dup={r['duplicate_cluster']}")
            if r['themes']: extra.append('themes='+','.join(r['themes']))
            extra.append(f"score={r['score']}")
            md.append(f"- `{r['path']}` — {', '.join(extra)} — {('; '.join(r['reasons']))[:220]}")
        if len(buckets[c])>80: md.append(f'- ... {len(buckets[c])-80} more in JSON')
        md.append('')
    (T/'PROJECT_PRIORITIZATION.md').write_text('\n'.join(md),encoding='utf-8')

    # one-file-per-category for quick opening
    for c in CATEGORIES:
        lines=[f'# {c}', '', f'Count: {len(buckets[c])}', '']
        for r in buckets[c]:
            lines.append(f"## {r['name']}")
            lines.append(f"Path: `{r['path']}`")
            lines.append(f"Score: {r['score']} | Dirty: {r['dirty_count']} | Branches: {r['branch_count']} | TODO/FIXME: {r['todo_fixme_count']} | Plans: {r['plan_hit_count']}")
            lines.append(f"Categories: {', '.join(r['categories'])}")
            if r['themes']: lines.append(f"Themes: {', '.join(r['themes'])}")
            if r['duplicate_cluster']: lines.append(f"Duplicate cluster: {r['duplicate_cluster']}")
            lines.append('Reasons:')
            for reason in r['reasons']: lines.append(f'- {reason}')
            lines.append('')
        (T/f'{c}.md').write_text('\n'.join(lines),encoding='utf-8')

    top10=[]
    for r in buckets['URGENT'][:5]:
        top10.append(('Stabilize dirty repo before more work', r))
    for r in buckets['MERGE'][:3]:
        top10.append(('Decide consolidation target', r))
    for r in buckets['KEEP'][:5]:
        if all(x[1]['path']!=r['path'] for x in top10): top10.append(('Keep and assign focused verifier', r))
    top10=top10[:10]
    lines=['# Top 10 Next Actions — Prioritized Categories','', '| Rank | Action | Category | Project | Why | Human gate |','|---:|---|---|---|---|---|']
    for i,(action,r) in enumerate(top10,1):
        cats=', '.join(r['categories'])
        why=', '.join(r['reasons'])[:180]
        gate='yes' if any(c in r['categories'] for c in ['URGENT','HUMAN_DECISION_REQUIRED','MERGE','ARCHIVE']) else 'no'
        lines.append(f"| {i} | {action} | {cats} | `{r['path']}` | {why} | {gate} |")
    (T/'TOP_10_NEXT_ACTIONS.md').write_text('\n'.join(lines)+'\n',encoding='utf-8')

    summary={c:len(buckets[c]) for c in CATEGORIES}
    summary['total_projects']=len(rows)
    (T/'PROJECT_PRIORITIZATION_SUMMARY.json').write_text(json.dumps(summary,indent=2),encoding='utf-8')
    print(json.dumps(summary,indent=2))

if __name__=='__main__': main()