smlflg's picture
Initial public upload from Projekte/AgentProjectMinenraeumer (part 9)
b67a1e9 verified
Raw History Blame Contribute Delete
8.23 kB
#!/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()