@oriro/orirocli 0.1.7 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (351) hide show
  1. package/ATTRIBUTION.md +8 -0
  2. package/LICENSE +21 -0
  3. package/dist/cli.js +35 -5
  4. package/package.json +1 -1
  5. package/skills/21stdev/SKILL.md +64 -0
  6. package/skills/graphify/SKILL.md +619 -0
  7. package/skills/graphify/__init__.py +28 -0
  8. package/skills/graphify/__main__.py +4582 -0
  9. package/skills/graphify/affected.py +154 -0
  10. package/skills/graphify/always_on/agents-md.md +12 -0
  11. package/skills/graphify/always_on/antigravity-rules.md +14 -0
  12. package/skills/graphify/always_on/claude-md.md +9 -0
  13. package/skills/graphify/always_on/gemini-md.md +9 -0
  14. package/skills/graphify/always_on/kiro-steering.md +5 -0
  15. package/skills/graphify/always_on/vscode-instructions.md +17 -0
  16. package/skills/graphify/analyze.py +724 -0
  17. package/skills/graphify/benchmark.py +155 -0
  18. package/skills/graphify/build.py +487 -0
  19. package/skills/graphify/cache.py +417 -0
  20. package/skills/graphify/callflow_html.py +2020 -0
  21. package/skills/graphify/cluster.py +272 -0
  22. package/skills/graphify/command-kilo.md +15 -0
  23. package/skills/graphify/dedup.py +429 -0
  24. package/skills/graphify/detect.py +1379 -0
  25. package/skills/graphify/diagnostics.py +390 -0
  26. package/skills/graphify/export.py +1408 -0
  27. package/skills/graphify/extract.py +11570 -0
  28. package/skills/graphify/global_graph.py +159 -0
  29. package/skills/graphify/google_workspace.py +223 -0
  30. package/skills/graphify/hooks.py +457 -0
  31. package/skills/graphify/ingest.py +331 -0
  32. package/skills/graphify/llm.py +1896 -0
  33. package/skills/graphify/manifest.py +4 -0
  34. package/skills/graphify/mcp_ingest.py +392 -0
  35. package/skills/graphify/multigraph_compat.py +212 -0
  36. package/skills/graphify/pg_introspect.py +142 -0
  37. package/skills/graphify/prs.py +748 -0
  38. package/skills/graphify/querylog.py +70 -0
  39. package/skills/graphify/report.py +218 -0
  40. package/skills/graphify/scip_ingest.py +363 -0
  41. package/skills/graphify/security.py +336 -0
  42. package/skills/graphify/semantic_cleanup.py +319 -0
  43. package/skills/graphify/serve.py +1309 -0
  44. package/skills/graphify/skill-aider.md +1246 -0
  45. package/skills/graphify/skill-amp.md +613 -0
  46. package/skills/graphify/skill-claw.md +616 -0
  47. package/skills/graphify/skill-codex.md +613 -0
  48. package/skills/graphify/skill-copilot.md +616 -0
  49. package/skills/graphify/skill-devin.md +1372 -0
  50. package/skills/graphify/skill-droid.md +613 -0
  51. package/skills/graphify/skill-kilo.md +625 -0
  52. package/skills/graphify/skill-kiro.md +615 -0
  53. package/skills/graphify/skill-opencode.md +608 -0
  54. package/skills/graphify/skill-pi.md +615 -0
  55. package/skills/graphify/skill-trae.md +614 -0
  56. package/skills/graphify/skill-vscode.md +612 -0
  57. package/skills/graphify/skill-windows.md +651 -0
  58. package/skills/graphify/skills/amp/references/add-watch.md +56 -0
  59. package/skills/graphify/skills/amp/references/exports.md +71 -0
  60. package/skills/graphify/skills/amp/references/extraction-spec.md +68 -0
  61. package/skills/graphify/skills/amp/references/github-and-merge.md +46 -0
  62. package/skills/graphify/skills/amp/references/hooks.md +33 -0
  63. package/skills/graphify/skills/amp/references/query.md +249 -0
  64. package/skills/graphify/skills/amp/references/transcribe.md +48 -0
  65. package/skills/graphify/skills/amp/references/update.md +179 -0
  66. package/skills/graphify/skills/claude/references/add-watch.md +56 -0
  67. package/skills/graphify/skills/claude/references/exports.md +71 -0
  68. package/skills/graphify/skills/claude/references/extraction-spec.md +68 -0
  69. package/skills/graphify/skills/claude/references/github-and-merge.md +46 -0
  70. package/skills/graphify/skills/claude/references/hooks.md +33 -0
  71. package/skills/graphify/skills/claude/references/query.md +103 -0
  72. package/skills/graphify/skills/claude/references/transcribe.md +48 -0
  73. package/skills/graphify/skills/claude/references/update.md +179 -0
  74. package/skills/graphify/skills/claw/references/add-watch.md +56 -0
  75. package/skills/graphify/skills/claw/references/exports.md +71 -0
  76. package/skills/graphify/skills/claw/references/extraction-spec.md +29 -0
  77. package/skills/graphify/skills/claw/references/github-and-merge.md +46 -0
  78. package/skills/graphify/skills/claw/references/hooks.md +33 -0
  79. package/skills/graphify/skills/claw/references/query.md +249 -0
  80. package/skills/graphify/skills/claw/references/transcribe.md +48 -0
  81. package/skills/graphify/skills/claw/references/update.md +179 -0
  82. package/skills/graphify/skills/codex/references/add-watch.md +56 -0
  83. package/skills/graphify/skills/codex/references/exports.md +71 -0
  84. package/skills/graphify/skills/codex/references/extraction-spec.md +29 -0
  85. package/skills/graphify/skills/codex/references/github-and-merge.md +46 -0
  86. package/skills/graphify/skills/codex/references/hooks.md +33 -0
  87. package/skills/graphify/skills/codex/references/query.md +249 -0
  88. package/skills/graphify/skills/codex/references/transcribe.md +48 -0
  89. package/skills/graphify/skills/codex/references/update.md +179 -0
  90. package/skills/graphify/skills/copilot/references/add-watch.md +56 -0
  91. package/skills/graphify/skills/copilot/references/exports.md +71 -0
  92. package/skills/graphify/skills/copilot/references/extraction-spec.md +68 -0
  93. package/skills/graphify/skills/copilot/references/github-and-merge.md +46 -0
  94. package/skills/graphify/skills/copilot/references/hooks.md +33 -0
  95. package/skills/graphify/skills/copilot/references/query.md +249 -0
  96. package/skills/graphify/skills/copilot/references/transcribe.md +48 -0
  97. package/skills/graphify/skills/copilot/references/update.md +179 -0
  98. package/skills/graphify/skills/droid/references/add-watch.md +56 -0
  99. package/skills/graphify/skills/droid/references/exports.md +71 -0
  100. package/skills/graphify/skills/droid/references/extraction-spec.md +68 -0
  101. package/skills/graphify/skills/droid/references/github-and-merge.md +46 -0
  102. package/skills/graphify/skills/droid/references/hooks.md +33 -0
  103. package/skills/graphify/skills/droid/references/query.md +249 -0
  104. package/skills/graphify/skills/droid/references/transcribe.md +48 -0
  105. package/skills/graphify/skills/droid/references/update.md +179 -0
  106. package/skills/graphify/skills/kilo/references/add-watch.md +56 -0
  107. package/skills/graphify/skills/kilo/references/exports.md +71 -0
  108. package/skills/graphify/skills/kilo/references/extraction-spec.md +68 -0
  109. package/skills/graphify/skills/kilo/references/github-and-merge.md +46 -0
  110. package/skills/graphify/skills/kilo/references/hooks.md +33 -0
  111. package/skills/graphify/skills/kilo/references/query.md +249 -0
  112. package/skills/graphify/skills/kilo/references/transcribe.md +48 -0
  113. package/skills/graphify/skills/kilo/references/update.md +179 -0
  114. package/skills/graphify/skills/kiro/references/add-watch.md +56 -0
  115. package/skills/graphify/skills/kiro/references/exports.md +71 -0
  116. package/skills/graphify/skills/kiro/references/extraction-spec.md +29 -0
  117. package/skills/graphify/skills/kiro/references/github-and-merge.md +46 -0
  118. package/skills/graphify/skills/kiro/references/hooks.md +33 -0
  119. package/skills/graphify/skills/kiro/references/query.md +249 -0
  120. package/skills/graphify/skills/kiro/references/transcribe.md +48 -0
  121. package/skills/graphify/skills/kiro/references/update.md +179 -0
  122. package/skills/graphify/skills/opencode/references/add-watch.md +56 -0
  123. package/skills/graphify/skills/opencode/references/exports.md +71 -0
  124. package/skills/graphify/skills/opencode/references/extraction-spec.md +68 -0
  125. package/skills/graphify/skills/opencode/references/github-and-merge.md +46 -0
  126. package/skills/graphify/skills/opencode/references/hooks.md +33 -0
  127. package/skills/graphify/skills/opencode/references/query.md +249 -0
  128. package/skills/graphify/skills/opencode/references/transcribe.md +48 -0
  129. package/skills/graphify/skills/opencode/references/update.md +179 -0
  130. package/skills/graphify/skills/pi/references/add-watch.md +56 -0
  131. package/skills/graphify/skills/pi/references/exports.md +71 -0
  132. package/skills/graphify/skills/pi/references/extraction-spec.md +29 -0
  133. package/skills/graphify/skills/pi/references/github-and-merge.md +46 -0
  134. package/skills/graphify/skills/pi/references/hooks.md +33 -0
  135. package/skills/graphify/skills/pi/references/query.md +249 -0
  136. package/skills/graphify/skills/pi/references/transcribe.md +48 -0
  137. package/skills/graphify/skills/pi/references/update.md +179 -0
  138. package/skills/graphify/skills/trae/references/add-watch.md +56 -0
  139. package/skills/graphify/skills/trae/references/exports.md +71 -0
  140. package/skills/graphify/skills/trae/references/extraction-spec.md +68 -0
  141. package/skills/graphify/skills/trae/references/github-and-merge.md +46 -0
  142. package/skills/graphify/skills/trae/references/hooks.md +35 -0
  143. package/skills/graphify/skills/trae/references/query.md +249 -0
  144. package/skills/graphify/skills/trae/references/transcribe.md +48 -0
  145. package/skills/graphify/skills/trae/references/update.md +179 -0
  146. package/skills/graphify/skills/vscode/references/add-watch.md +56 -0
  147. package/skills/graphify/skills/vscode/references/exports.md +71 -0
  148. package/skills/graphify/skills/vscode/references/extraction-spec.md +68 -0
  149. package/skills/graphify/skills/vscode/references/github-and-merge.md +46 -0
  150. package/skills/graphify/skills/vscode/references/hooks.md +33 -0
  151. package/skills/graphify/skills/vscode/references/query.md +249 -0
  152. package/skills/graphify/skills/vscode/references/transcribe.md +48 -0
  153. package/skills/graphify/skills/vscode/references/update.md +179 -0
  154. package/skills/graphify/skills/windows/references/add-watch.md +56 -0
  155. package/skills/graphify/skills/windows/references/exports.md +71 -0
  156. package/skills/graphify/skills/windows/references/extraction-spec.md +68 -0
  157. package/skills/graphify/skills/windows/references/github-and-merge.md +46 -0
  158. package/skills/graphify/skills/windows/references/hooks.md +33 -0
  159. package/skills/graphify/skills/windows/references/query.md +249 -0
  160. package/skills/graphify/skills/windows/references/transcribe.md +48 -0
  161. package/skills/graphify/skills/windows/references/update.md +179 -0
  162. package/skills/graphify/symbol_resolution.py +538 -0
  163. package/skills/graphify/transcribe.py +184 -0
  164. package/skills/graphify/tree_html.py +582 -0
  165. package/skills/graphify/validate.py +72 -0
  166. package/skills/graphify/watch.py +898 -0
  167. package/skills/graphify/wiki.py +282 -0
  168. package/skills/impeccable/SKILL.md +186 -0
  169. package/skills/impeccable/agents/impeccable_asset_producer.toml +92 -0
  170. package/skills/impeccable/agents/impeccable_manual_edit_applier.toml +95 -0
  171. package/skills/impeccable/agents/openai.yaml +4 -0
  172. package/skills/impeccable/reference/adapt.md +311 -0
  173. package/skills/impeccable/reference/animate.md +201 -0
  174. package/skills/impeccable/reference/audit.md +133 -0
  175. package/skills/impeccable/reference/bolder.md +113 -0
  176. package/skills/impeccable/reference/brand.md +108 -0
  177. package/skills/impeccable/reference/clarify.md +288 -0
  178. package/skills/impeccable/reference/codex.md +105 -0
  179. package/skills/impeccable/reference/colorize.md +257 -0
  180. package/skills/impeccable/reference/craft.md +123 -0
  181. package/skills/impeccable/reference/critique.md +790 -0
  182. package/skills/impeccable/reference/delight.md +302 -0
  183. package/skills/impeccable/reference/distill.md +111 -0
  184. package/skills/impeccable/reference/document.md +429 -0
  185. package/skills/impeccable/reference/extract.md +69 -0
  186. package/skills/impeccable/reference/harden.md +347 -0
  187. package/skills/impeccable/reference/init.md +172 -0
  188. package/skills/impeccable/reference/interaction-design.md +189 -0
  189. package/skills/impeccable/reference/layout.md +161 -0
  190. package/skills/impeccable/reference/live.md +720 -0
  191. package/skills/impeccable/reference/onboard.md +234 -0
  192. package/skills/impeccable/reference/optimize.md +258 -0
  193. package/skills/impeccable/reference/overdrive.md +130 -0
  194. package/skills/impeccable/reference/polish.md +241 -0
  195. package/skills/impeccable/reference/product.md +60 -0
  196. package/skills/impeccable/reference/quieter.md +99 -0
  197. package/skills/impeccable/reference/shape.md +165 -0
  198. package/skills/impeccable/reference/typeset.md +279 -0
  199. package/skills/impeccable/scripts/cleanup-deprecated.mjs +284 -0
  200. package/skills/impeccable/scripts/command-metadata.json +94 -0
  201. package/skills/impeccable/scripts/context-signals.mjs +225 -0
  202. package/skills/impeccable/scripts/context.mjs +266 -0
  203. package/skills/impeccable/scripts/critique-storage.mjs +242 -0
  204. package/skills/impeccable/scripts/design-parser.mjs +835 -0
  205. package/skills/impeccable/scripts/detect-csp.mjs +198 -0
  206. package/skills/impeccable/scripts/detect.mjs +21 -0
  207. package/skills/impeccable/scripts/detector/browser/injected/index.mjs +1733 -0
  208. package/skills/impeccable/scripts/detector/cli/main.mjs +244 -0
  209. package/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +4618 -0
  210. package/skills/impeccable/scripts/detector/detect-antipatterns.mjs +43 -0
  211. package/skills/impeccable/scripts/detector/engines/browser/detect-url.mjs +252 -0
  212. package/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs +535 -0
  213. package/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs +986 -0
  214. package/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs +208 -0
  215. package/skills/impeccable/scripts/detector/engines/visual/screenshot-contrast.mjs +189 -0
  216. package/skills/impeccable/scripts/detector/findings.mjs +12 -0
  217. package/skills/impeccable/scripts/detector/node/file-system.mjs +198 -0
  218. package/skills/impeccable/scripts/detector/profile/profiler.mjs +166 -0
  219. package/skills/impeccable/scripts/detector/registry/antipatterns.mjs +419 -0
  220. package/skills/impeccable/scripts/detector/rules/checks.mjs +2384 -0
  221. package/skills/impeccable/scripts/detector/shared/color.mjs +124 -0
  222. package/skills/impeccable/scripts/detector/shared/constants.mjs +101 -0
  223. package/skills/impeccable/scripts/detector/shared/page.mjs +7 -0
  224. package/skills/impeccable/scripts/impeccable-paths.mjs +126 -0
  225. package/skills/impeccable/scripts/is-generated.mjs +69 -0
  226. package/skills/impeccable/scripts/live-accept.mjs +812 -0
  227. package/skills/impeccable/scripts/live-browser-session.js +123 -0
  228. package/skills/impeccable/scripts/live-browser.js +10295 -0
  229. package/skills/impeccable/scripts/live-commit-manual-edits.mjs +1241 -0
  230. package/skills/impeccable/scripts/live-complete.mjs +75 -0
  231. package/skills/impeccable/scripts/live-completion.mjs +19 -0
  232. package/skills/impeccable/scripts/live-copy-edit-agent.mjs +683 -0
  233. package/skills/impeccable/scripts/live-discard-manual-edits.mjs +51 -0
  234. package/skills/impeccable/scripts/live-event-validation.mjs +137 -0
  235. package/skills/impeccable/scripts/live-inject.mjs +557 -0
  236. package/skills/impeccable/scripts/live-insert-ui.mjs +458 -0
  237. package/skills/impeccable/scripts/live-insert.mjs +272 -0
  238. package/skills/impeccable/scripts/live-manual-edit-evidence.mjs +363 -0
  239. package/skills/impeccable/scripts/live-manual-edits-buffer.mjs +152 -0
  240. package/skills/impeccable/scripts/live-poll.mjs +379 -0
  241. package/skills/impeccable/scripts/live-resume.mjs +94 -0
  242. package/skills/impeccable/scripts/live-server.mjs +2326 -0
  243. package/skills/impeccable/scripts/live-session-store.mjs +289 -0
  244. package/skills/impeccable/scripts/live-status.mjs +61 -0
  245. package/skills/impeccable/scripts/live-svelte-component.mjs +826 -0
  246. package/skills/impeccable/scripts/live-sveltekit-adapter.mjs +274 -0
  247. package/skills/impeccable/scripts/live-ui-core.mjs +179 -0
  248. package/skills/impeccable/scripts/live-vocabulary.mjs +36 -0
  249. package/skills/impeccable/scripts/live-wrap.mjs +894 -0
  250. package/skills/impeccable/scripts/live.mjs +246 -0
  251. package/skills/impeccable/scripts/modern-screenshot.umd.js +14 -0
  252. package/skills/impeccable/scripts/palette.mjs +633 -0
  253. package/skills/impeccable/scripts/pin.mjs +214 -0
  254. package/skills/uipm-ui-styling/LICENSE.txt +202 -0
  255. package/skills/uipm-ui-styling/SKILL.md +328 -0
  256. package/skills/uipm-ui-styling/canvas-fonts/ArsenalSC-OFL.txt +93 -0
  257. package/skills/uipm-ui-styling/canvas-fonts/ArsenalSC-Regular.ttf +0 -0
  258. package/skills/uipm-ui-styling/canvas-fonts/BigShoulders-Bold.ttf +0 -0
  259. package/skills/uipm-ui-styling/canvas-fonts/BigShoulders-OFL.txt +93 -0
  260. package/skills/uipm-ui-styling/canvas-fonts/BigShoulders-Regular.ttf +0 -0
  261. package/skills/uipm-ui-styling/canvas-fonts/Boldonse-OFL.txt +93 -0
  262. package/skills/uipm-ui-styling/canvas-fonts/Boldonse-Regular.ttf +0 -0
  263. package/skills/uipm-ui-styling/canvas-fonts/BricolageGrotesque-Bold.ttf +0 -0
  264. package/skills/uipm-ui-styling/canvas-fonts/BricolageGrotesque-OFL.txt +93 -0
  265. package/skills/uipm-ui-styling/canvas-fonts/BricolageGrotesque-Regular.ttf +0 -0
  266. package/skills/uipm-ui-styling/canvas-fonts/CrimsonPro-Bold.ttf +0 -0
  267. package/skills/uipm-ui-styling/canvas-fonts/CrimsonPro-Italic.ttf +0 -0
  268. package/skills/uipm-ui-styling/canvas-fonts/CrimsonPro-OFL.txt +93 -0
  269. package/skills/uipm-ui-styling/canvas-fonts/CrimsonPro-Regular.ttf +0 -0
  270. package/skills/uipm-ui-styling/canvas-fonts/DMMono-OFL.txt +93 -0
  271. package/skills/uipm-ui-styling/canvas-fonts/DMMono-Regular.ttf +0 -0
  272. package/skills/uipm-ui-styling/canvas-fonts/EricaOne-OFL.txt +94 -0
  273. package/skills/uipm-ui-styling/canvas-fonts/EricaOne-Regular.ttf +0 -0
  274. package/skills/uipm-ui-styling/canvas-fonts/GeistMono-Bold.ttf +0 -0
  275. package/skills/uipm-ui-styling/canvas-fonts/GeistMono-OFL.txt +93 -0
  276. package/skills/uipm-ui-styling/canvas-fonts/GeistMono-Regular.ttf +0 -0
  277. package/skills/uipm-ui-styling/canvas-fonts/Gloock-OFL.txt +93 -0
  278. package/skills/uipm-ui-styling/canvas-fonts/Gloock-Regular.ttf +0 -0
  279. package/skills/uipm-ui-styling/canvas-fonts/IBMPlexMono-Bold.ttf +0 -0
  280. package/skills/uipm-ui-styling/canvas-fonts/IBMPlexMono-OFL.txt +93 -0
  281. package/skills/uipm-ui-styling/canvas-fonts/IBMPlexMono-Regular.ttf +0 -0
  282. package/skills/uipm-ui-styling/canvas-fonts/IBMPlexSerif-Bold.ttf +0 -0
  283. package/skills/uipm-ui-styling/canvas-fonts/IBMPlexSerif-BoldItalic.ttf +0 -0
  284. package/skills/uipm-ui-styling/canvas-fonts/IBMPlexSerif-Italic.ttf +0 -0
  285. package/skills/uipm-ui-styling/canvas-fonts/IBMPlexSerif-Regular.ttf +0 -0
  286. package/skills/uipm-ui-styling/canvas-fonts/InstrumentSans-Bold.ttf +0 -0
  287. package/skills/uipm-ui-styling/canvas-fonts/InstrumentSans-BoldItalic.ttf +0 -0
  288. package/skills/uipm-ui-styling/canvas-fonts/InstrumentSans-Italic.ttf +0 -0
  289. package/skills/uipm-ui-styling/canvas-fonts/InstrumentSans-OFL.txt +93 -0
  290. package/skills/uipm-ui-styling/canvas-fonts/InstrumentSans-Regular.ttf +0 -0
  291. package/skills/uipm-ui-styling/canvas-fonts/InstrumentSerif-Italic.ttf +0 -0
  292. package/skills/uipm-ui-styling/canvas-fonts/InstrumentSerif-Regular.ttf +0 -0
  293. package/skills/uipm-ui-styling/canvas-fonts/Italiana-OFL.txt +93 -0
  294. package/skills/uipm-ui-styling/canvas-fonts/Italiana-Regular.ttf +0 -0
  295. package/skills/uipm-ui-styling/canvas-fonts/JetBrainsMono-Bold.ttf +0 -0
  296. package/skills/uipm-ui-styling/canvas-fonts/JetBrainsMono-OFL.txt +93 -0
  297. package/skills/uipm-ui-styling/canvas-fonts/JetBrainsMono-Regular.ttf +0 -0
  298. package/skills/uipm-ui-styling/canvas-fonts/Jura-Light.ttf +0 -0
  299. package/skills/uipm-ui-styling/canvas-fonts/Jura-Medium.ttf +0 -0
  300. package/skills/uipm-ui-styling/canvas-fonts/Jura-OFL.txt +93 -0
  301. package/skills/uipm-ui-styling/canvas-fonts/LibreBaskerville-OFL.txt +93 -0
  302. package/skills/uipm-ui-styling/canvas-fonts/LibreBaskerville-Regular.ttf +0 -0
  303. package/skills/uipm-ui-styling/canvas-fonts/Lora-Bold.ttf +0 -0
  304. package/skills/uipm-ui-styling/canvas-fonts/Lora-BoldItalic.ttf +0 -0
  305. package/skills/uipm-ui-styling/canvas-fonts/Lora-Italic.ttf +0 -0
  306. package/skills/uipm-ui-styling/canvas-fonts/Lora-OFL.txt +93 -0
  307. package/skills/uipm-ui-styling/canvas-fonts/Lora-Regular.ttf +0 -0
  308. package/skills/uipm-ui-styling/canvas-fonts/NationalPark-Bold.ttf +0 -0
  309. package/skills/uipm-ui-styling/canvas-fonts/NationalPark-OFL.txt +93 -0
  310. package/skills/uipm-ui-styling/canvas-fonts/NationalPark-Regular.ttf +0 -0
  311. package/skills/uipm-ui-styling/canvas-fonts/NothingYouCouldDo-OFL.txt +93 -0
  312. package/skills/uipm-ui-styling/canvas-fonts/NothingYouCouldDo-Regular.ttf +0 -0
  313. package/skills/uipm-ui-styling/canvas-fonts/Outfit-Bold.ttf +0 -0
  314. package/skills/uipm-ui-styling/canvas-fonts/Outfit-OFL.txt +93 -0
  315. package/skills/uipm-ui-styling/canvas-fonts/Outfit-Regular.ttf +0 -0
  316. package/skills/uipm-ui-styling/canvas-fonts/PixelifySans-Medium.ttf +0 -0
  317. package/skills/uipm-ui-styling/canvas-fonts/PixelifySans-OFL.txt +93 -0
  318. package/skills/uipm-ui-styling/canvas-fonts/PoiretOne-OFL.txt +93 -0
  319. package/skills/uipm-ui-styling/canvas-fonts/PoiretOne-Regular.ttf +0 -0
  320. package/skills/uipm-ui-styling/canvas-fonts/RedHatMono-Bold.ttf +0 -0
  321. package/skills/uipm-ui-styling/canvas-fonts/RedHatMono-OFL.txt +93 -0
  322. package/skills/uipm-ui-styling/canvas-fonts/RedHatMono-Regular.ttf +0 -0
  323. package/skills/uipm-ui-styling/canvas-fonts/Silkscreen-OFL.txt +93 -0
  324. package/skills/uipm-ui-styling/canvas-fonts/Silkscreen-Regular.ttf +0 -0
  325. package/skills/uipm-ui-styling/canvas-fonts/SmoochSans-Medium.ttf +0 -0
  326. package/skills/uipm-ui-styling/canvas-fonts/SmoochSans-OFL.txt +93 -0
  327. package/skills/uipm-ui-styling/canvas-fonts/Tektur-Medium.ttf +0 -0
  328. package/skills/uipm-ui-styling/canvas-fonts/Tektur-OFL.txt +93 -0
  329. package/skills/uipm-ui-styling/canvas-fonts/Tektur-Regular.ttf +0 -0
  330. package/skills/uipm-ui-styling/canvas-fonts/WorkSans-Bold.ttf +0 -0
  331. package/skills/uipm-ui-styling/canvas-fonts/WorkSans-BoldItalic.ttf +0 -0
  332. package/skills/uipm-ui-styling/canvas-fonts/WorkSans-Italic.ttf +0 -0
  333. package/skills/uipm-ui-styling/canvas-fonts/WorkSans-OFL.txt +93 -0
  334. package/skills/uipm-ui-styling/canvas-fonts/WorkSans-Regular.ttf +0 -0
  335. package/skills/uipm-ui-styling/canvas-fonts/YoungSerif-OFL.txt +93 -0
  336. package/skills/uipm-ui-styling/canvas-fonts/YoungSerif-Regular.ttf +0 -0
  337. package/skills/uipm-ui-styling/references/canvas-design-system.md +320 -0
  338. package/skills/uipm-ui-styling/references/shadcn-accessibility.md +471 -0
  339. package/skills/uipm-ui-styling/references/shadcn-components.md +424 -0
  340. package/skills/uipm-ui-styling/references/shadcn-theming.md +373 -0
  341. package/skills/uipm-ui-styling/references/tailwind-customization.md +483 -0
  342. package/skills/uipm-ui-styling/references/tailwind-responsive.md +382 -0
  343. package/skills/uipm-ui-styling/references/tailwind-utilities.md +455 -0
  344. package/skills/uipm-ui-styling/scripts/.coverage +0 -0
  345. package/skills/uipm-ui-styling/scripts/requirements.txt +17 -0
  346. package/skills/uipm-ui-styling/scripts/shadcn_add.py +292 -0
  347. package/skills/uipm-ui-styling/scripts/tailwind_config_gen.py +456 -0
  348. package/skills/uipm-ui-styling/scripts/tests/coverage-ui.json +1 -0
  349. package/skills/uipm-ui-styling/scripts/tests/requirements.txt +3 -0
  350. package/skills/uipm-ui-styling/scripts/tests/test_shadcn_add.py +266 -0
  351. package/skills/uipm-ui-styling/scripts/tests/test_tailwind_config_gen.py +336 -0
@@ -0,0 +1,748 @@
1
+ """graphify prs — graph-aware PR dashboard.
2
+
3
+ Fast terminal overview of open PRs with CI/review state, worktree mapping,
4
+ and optional graph-impact analysis (which communities a PR touches) and
5
+ Opus-powered triage ranking.
6
+
7
+ Usage:
8
+ graphify prs # dashboard of all open PRs
9
+ graphify prs <number> # deep dive on one PR
10
+ graphify prs --triage # Opus ranks your review queue
11
+ graphify prs --worktrees # show worktree → branch → PR mapping
12
+ graphify prs --conflicts # PRs sharing graph communities (merge-order risk)
13
+ graphify prs --base <branch> # filter to PRs targeting this base (default: v8)
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import json
19
+ import os
20
+ import re
21
+ import subprocess
22
+ import sys
23
+ from collections import defaultdict
24
+ from concurrent.futures import ThreadPoolExecutor, as_completed
25
+ from dataclasses import dataclass, field
26
+ from datetime import datetime, timezone
27
+ from pathlib import Path
28
+
29
+
30
+ # ── ANSI colours ─────────────────────────────────────────────────────────────
31
+
32
+ _NO_COLOR = not sys.stdout.isatty() or os.environ.get("NO_COLOR")
33
+
34
+ def _c(code: str, text: str) -> str:
35
+ if _NO_COLOR:
36
+ return text
37
+ return f"\033[{code}m{text}\033[0m"
38
+
39
+ def green(t: str) -> str: return _c("32", t)
40
+ def red(t: str) -> str: return _c("31", t)
41
+ def yellow(t: str) -> str: return _c("33", t)
42
+ def cyan(t: str) -> str: return _c("36", t)
43
+ def bold(t: str) -> str: return _c("1", t)
44
+ def dim(t: str) -> str: return _c("2", t)
45
+ def magenta(t: str) -> str: return _c("35", t)
46
+
47
+ _ANSI_RE = re.compile(r"\033\[[0-9;]*m")
48
+
49
+ def _pad(s: str, width: int) -> str:
50
+ """Pad an ANSI-colored string to visible width (strips escape codes for length calc)."""
51
+ visible_len = len(_ANSI_RE.sub("", s))
52
+ return s + " " * max(0, width - visible_len)
53
+
54
+
55
+ # ── Data model ────────────────────────────────────────────────────────────────
56
+
57
+ @dataclass
58
+ class PRInfo:
59
+ number: int
60
+ title: str
61
+ branch: str
62
+ base_branch: str
63
+ author: str
64
+ is_draft: bool
65
+ review_decision: str # APPROVED | CHANGES_REQUESTED | ""
66
+ ci_status: str # SUCCESS | FAILURE | PENDING | NONE
67
+ updated_at: datetime
68
+ expected_base: str = "main" # set by fetch_prs via _detect_default_branch
69
+ worktree_path: str | None = None
70
+ # Graph impact — populated when graph.json exists
71
+ communities_touched: list[int] = field(default_factory=list)
72
+ nodes_affected: int = 0
73
+ files_changed: list[str] = field(default_factory=list)
74
+
75
+ @property
76
+ def status(self) -> str:
77
+ return _classify(self, self.expected_base)
78
+
79
+ @property
80
+ def days_old(self) -> int:
81
+ return (datetime.now(timezone.utc) - self.updated_at).days
82
+
83
+ @property
84
+ def blast_radius(self) -> str:
85
+ if not self.nodes_affected:
86
+ return ""
87
+ n = self.nodes_affected
88
+ c = len(self.communities_touched)
89
+ return f"{n} node{'s' if n != 1 else ''} / {c} communit{'ies' if c != 1 else 'y'}"
90
+
91
+
92
+ # ── Classification ────────────────────────────────────────────────────────────
93
+
94
+ _STATUS_ORDER = ["WRONG-BASE", "CI-FAIL", "CHANGES-REQ", "DRAFT", "STALE", "PENDING", "APPROVED", "READY"]
95
+ _STALE_DAYS = 14
96
+
97
+
98
+ def _classify(pr: "PRInfo", base: str = "v8") -> str:
99
+ if pr.base_branch != base:
100
+ return "WRONG-BASE"
101
+ if pr.ci_status == "FAILURE":
102
+ return "CI-FAIL"
103
+ if pr.review_decision == "CHANGES_REQUESTED":
104
+ return "CHANGES-REQ"
105
+ if pr.is_draft:
106
+ return "DRAFT"
107
+ if pr.days_old >= _STALE_DAYS:
108
+ return "STALE"
109
+ if pr.review_decision == "APPROVED":
110
+ return "APPROVED"
111
+ if pr.ci_status == "PENDING":
112
+ return "PENDING"
113
+ return "READY"
114
+
115
+
116
+ def _status_color(status: str) -> str:
117
+ return {
118
+ "READY": green(status),
119
+ "APPROVED": bold(green(status)),
120
+ "CI-FAIL": red(status),
121
+ "CHANGES-REQ": red(status),
122
+ "WRONG-BASE": dim(status),
123
+ "STALE": dim(status),
124
+ "DRAFT": yellow(status),
125
+ "PENDING": yellow(status),
126
+ }.get(status, status)
127
+
128
+
129
+ def _ci_icon(status: str) -> str:
130
+ return {"SUCCESS": green("✓"), "FAILURE": red("✗"), "PENDING": yellow("…"), "NONE": dim("–")}.get(status, "?")
131
+
132
+
133
+ # ── GitHub data fetching ──────────────────────────────────────────────────────
134
+
135
+ def _gh(*args: str) -> list | dict | None:
136
+ try:
137
+ result = subprocess.run(
138
+ ["gh", *args],
139
+ capture_output=True, text=True, timeout=30
140
+ )
141
+ if result.returncode != 0:
142
+ return None
143
+ return json.loads(result.stdout)
144
+ except (subprocess.TimeoutExpired, json.JSONDecodeError, FileNotFoundError):
145
+ return None
146
+
147
+
148
+ def _detect_default_branch(repo: str | None = None) -> str:
149
+ """Auto-detect the repo's default branch via gh, then git, then fall back to 'main'."""
150
+ # Try gh first — works for any repo, not just the current directory
151
+ args = ["repo", "view", "--json", "defaultBranchRef"]
152
+ if repo:
153
+ args += ["--repo", repo]
154
+ data = _gh(*args)
155
+ if data and data.get("defaultBranchRef", {}).get("name"):
156
+ return data["defaultBranchRef"]["name"]
157
+ # Fall back to git symbolic-ref for the current repo
158
+ try:
159
+ result = subprocess.run(
160
+ ["git", "symbolic-ref", "refs/remotes/origin/HEAD"],
161
+ capture_output=True, text=True, timeout=5
162
+ )
163
+ if result.returncode == 0:
164
+ # refs/remotes/origin/main → main
165
+ ref = result.stdout.strip()
166
+ return ref.split("/")[-1] if ref else "main"
167
+ except (subprocess.TimeoutExpired, FileNotFoundError):
168
+ pass
169
+ return "main"
170
+
171
+
172
+ _CI_FAILURE_CONCLUSIONS = frozenset({"FAILURE", "CANCELLED", "TIMED_OUT", "ACTION_REQUIRED", "STARTUP_FAILURE"})
173
+
174
+
175
+ def _parse_ci(rollup: list) -> str:
176
+ if not rollup:
177
+ return "NONE"
178
+ conclusions = {r.get("conclusion") for r in rollup if r.get("conclusion")}
179
+ if conclusions & _CI_FAILURE_CONCLUSIONS:
180
+ return "FAILURE"
181
+ statuses = {r.get("status") for r in rollup}
182
+ if "IN_PROGRESS" in statuses or "QUEUED" in statuses:
183
+ return "PENDING"
184
+ if "SUCCESS" in conclusions:
185
+ return "SUCCESS"
186
+ return "NONE"
187
+
188
+
189
+ def fetch_prs(repo: str | None = None, base: str | None = None, limit: int = 50) -> list[PRInfo]:
190
+ resolved_base = base or _detect_default_branch(repo)
191
+ args = [
192
+ "pr", "list", "--state", "open", "--limit", str(limit),
193
+ "--json", "number,title,headRefName,baseRefName,author,isDraft,"
194
+ "reviewDecision,statusCheckRollup,updatedAt",
195
+ ]
196
+ if repo:
197
+ args += ["--repo", repo]
198
+
199
+ raw = _gh(*args)
200
+ if raw is None:
201
+ raise RuntimeError("gh CLI not found or not authenticated. Run: gh auth login")
202
+
203
+ prs = []
204
+ for item in raw:
205
+ updated = datetime.fromisoformat(item["updatedAt"].replace("Z", "+00:00"))
206
+ prs.append(PRInfo(
207
+ number=item["number"],
208
+ title=item["title"],
209
+ branch=item["headRefName"],
210
+ base_branch=item["baseRefName"],
211
+ author=item["author"]["login"] if item.get("author") else "?",
212
+ is_draft=item.get("isDraft", False),
213
+ review_decision=item.get("reviewDecision") or "",
214
+ ci_status=_parse_ci(item.get("statusCheckRollup") or []),
215
+ updated_at=updated,
216
+ expected_base=resolved_base,
217
+ ))
218
+ return prs
219
+
220
+
221
+ def fetch_pr_files(number: int, repo: str | None = None) -> list[str]:
222
+ args = ["pr", "diff", str(number), "--name-only"]
223
+ if repo:
224
+ args += ["--repo", repo]
225
+ try:
226
+ result = subprocess.run(["gh", *args], capture_output=True, text=True, timeout=30)
227
+ if result.returncode != 0:
228
+ return []
229
+ return [l.strip() for l in result.stdout.splitlines() if l.strip()]
230
+ except (subprocess.TimeoutExpired, FileNotFoundError):
231
+ return []
232
+
233
+
234
+ # ── Graph-native impact (used by MCP tools — works on nx.Graph directly) ─────
235
+
236
+ def _path_match(graph_src: str, pr_file: str) -> bool:
237
+ """True if graph_src and pr_file refer to the same file (path-boundary safe)."""
238
+ if graph_src == pr_file:
239
+ return True
240
+ return graph_src.endswith("/" + pr_file) or pr_file.endswith("/" + graph_src)
241
+
242
+
243
+ def compute_pr_impact(files: list[str], G: "nx.Graph") -> tuple[list[int], int]:
244
+ """Return (communities_touched, nodes_affected) for a set of changed files.
245
+
246
+ Builds a file→(communities, count) index first so lookup is O(nodes + files)
247
+ rather than O(nodes × files).
248
+ """
249
+ # Build index once
250
+ file_comms: dict[str, set[int]] = {}
251
+ file_count: dict[str, int] = {}
252
+ for _, data in G.nodes(data=True):
253
+ src = data.get("source_file") or ""
254
+ if not src:
255
+ continue
256
+ if src not in file_comms:
257
+ file_comms[src] = set()
258
+ file_count[src] = 0
259
+ c = data.get("community")
260
+ if c is not None:
261
+ file_comms[src].add(int(c))
262
+ file_count[src] += 1
263
+
264
+ comms: set[int] = set()
265
+ nodes = 0
266
+ matched: set[str] = set()
267
+ for f in files:
268
+ for src, src_comms in file_comms.items():
269
+ if src not in matched and _path_match(src, f):
270
+ comms |= src_comms
271
+ nodes += file_count[src]
272
+ matched.add(src)
273
+ return sorted(comms), nodes
274
+
275
+
276
+ def format_prs_text(prs: list["PRInfo"], base: str) -> str:
277
+ """Plain-text PR summary for MCP output (no ANSI)."""
278
+ actionable = [p for p in prs if p.base_branch == base]
279
+ wrong = len(prs) - len(actionable)
280
+ lines = [f"Open PRs targeting {base}: {len(actionable)} ({wrong} on wrong base, not shown)\n"]
281
+ for p in sorted(actionable, key=lambda x: (_STATUS_ORDER.index(x.status) if x.status in _STATUS_ORDER else 99, x.days_old)):
282
+ impact = f" blast_radius={p.blast_radius}" if p.blast_radius else ""
283
+ lines.append(
284
+ f"#{p.number} [{p.status}] CI={p.ci_status} review={p.review_decision or 'none'} "
285
+ f"age={p.days_old}d author={p.author}{impact}\n {p.title}"
286
+ )
287
+ return "\n\n".join(lines)
288
+
289
+
290
+ # ── Worktree mapping ──────────────────────────────────────────────────────────
291
+
292
+ def fetch_worktrees() -> dict[str, str]:
293
+ """Returns {branch: worktree_path}."""
294
+ try:
295
+ result = subprocess.run(
296
+ ["git", "worktree", "list", "--porcelain"],
297
+ capture_output=True, text=True, timeout=10
298
+ )
299
+ if result.returncode != 0:
300
+ return {}
301
+ except (subprocess.TimeoutExpired, FileNotFoundError):
302
+ return {}
303
+
304
+ mapping: dict[str, str] = {}
305
+ current_path = None
306
+ for line in result.stdout.splitlines():
307
+ if not line:
308
+ current_path = None # blank line = record separator; reset to avoid leaking across detached HEADs
309
+ elif line.startswith("worktree "):
310
+ current_path = line[9:]
311
+ elif line.startswith("branch refs/heads/") and current_path:
312
+ mapping[line[18:]] = current_path
313
+ return mapping
314
+
315
+
316
+ # ── Graph impact analysis ─────────────────────────────────────────────────────
317
+
318
+ def _load_graph_json(graph_path: Path) -> dict | None:
319
+ if not graph_path.exists():
320
+ return None
321
+ from graphify.security import check_graph_file_size_cap
322
+ try:
323
+ check_graph_file_size_cap(graph_path)
324
+ return json.loads(graph_path.read_text(encoding="utf-8"))
325
+ except (json.JSONDecodeError, OSError, ValueError):
326
+ return None
327
+
328
+
329
+ def build_community_labels(data: dict, top_n: int = 4) -> dict[int, list[str]]:
330
+ """Return {community_id: [top_labels]} extracted from graph node data."""
331
+ comm_labels: dict[int, list[str]] = defaultdict(list)
332
+ for node in data.get("nodes", []):
333
+ c = node.get("community")
334
+ if c is None:
335
+ continue
336
+ label = node.get("label") or node.get("id") or ""
337
+ if label:
338
+ comm_labels[int(c)].append(label)
339
+ return {c: labels[:top_n] for c, labels in comm_labels.items()}
340
+
341
+
342
+ def attach_graph_impact(
343
+ prs: list[PRInfo], graph_path: Path, repo: str | None = None
344
+ ) -> dict[int, list[str]]:
345
+ """Fetch PR file lists concurrently, compute graph impact, return community labels."""
346
+ data = _load_graph_json(graph_path)
347
+ if not data:
348
+ return {}
349
+
350
+ # Build file → {community, node_count} index
351
+ file_to_communities: dict[str, set[int]] = {}
352
+ file_to_nodes: dict[str, int] = {}
353
+ for node in data.get("nodes", []):
354
+ src = node.get("source_file") or ""
355
+ if not src:
356
+ continue
357
+ comm = node.get("community")
358
+ if src not in file_to_communities:
359
+ file_to_communities[src] = set()
360
+ file_to_nodes[src] = 0
361
+ if comm is not None:
362
+ file_to_communities[src].add(int(comm))
363
+ file_to_nodes[src] += 1
364
+
365
+ # Fetch diffs concurrently — gh pr diff is the bottleneck (network I/O)
366
+ actionable = [pr for pr in prs if pr.status != "WRONG-BASE"]
367
+ workers = min(8, len(actionable)) if actionable else 1
368
+ with ThreadPoolExecutor(max_workers=workers) as pool:
369
+ future_to_pr = {
370
+ pool.submit(fetch_pr_files, pr.number, repo): pr
371
+ for pr in actionable
372
+ }
373
+ for fut in as_completed(future_to_pr):
374
+ pr = future_to_pr[fut]
375
+ try:
376
+ files = fut.result()
377
+ except Exception:
378
+ files = []
379
+ pr.files_changed = files
380
+
381
+ comms: set[int] = set()
382
+ nodes = 0
383
+ matched: set[str] = set()
384
+ for f in files:
385
+ for gf, gcomms in file_to_communities.items():
386
+ if gf not in matched and _path_match(gf, f):
387
+ comms |= gcomms
388
+ nodes += file_to_nodes.get(gf, 0)
389
+ matched.add(gf)
390
+ pr.communities_touched = sorted(comms)
391
+ pr.nodes_affected = nodes
392
+
393
+ return build_community_labels(data)
394
+
395
+
396
+ # ── Dashboard rendering ───────────────────────────────────────────────────────
397
+
398
+ def _truncate(s: str, n: int) -> str:
399
+ return s if len(s) <= n else s[:n - 1] + "…"
400
+
401
+
402
+ def render_dashboard(prs: list[PRInfo], base: str = "v8", show_wrong_base: bool = False) -> None:
403
+ actionable = [p for p in prs if p.base_branch == base]
404
+ wrong_base = [p for p in prs if p.base_branch != base]
405
+
406
+ # Sort: READY first, then by status order, then by recency
407
+ actionable.sort(key=lambda p: (_STATUS_ORDER.index(p.status) if p.status in _STATUS_ORDER else 99, p.days_old))
408
+
409
+ print()
410
+ print(bold(f" graphify prs · base: {base} · {len(actionable)} PRs"))
411
+ print()
412
+
413
+ if not actionable:
414
+ print(dim(" No open PRs targeting this base branch."))
415
+ else:
416
+ # Header
417
+ print(f" {'#':>4} {'CI':2} {'STATUS':13} {'UPDATED':8} {'IMPACT':22} TITLE")
418
+ print(f" {'─'*4} {'─'*2} {'─'*13} {'─'*8} {'─'*22} {'─'*40}")
419
+
420
+ for pr in actionable:
421
+ status_str = _pad(_status_color(pr.status), 13)
422
+ ci_str = _ci_icon(pr.ci_status)
423
+ age = f"{pr.days_old}d" if pr.days_old > 0 else "today"
424
+ impact = _pad(dim(_truncate(pr.blast_radius, 22)), 22) if pr.blast_radius else _pad(dim("–"), 22)
425
+ wt = f" {cyan('⬡')}" if pr.worktree_path else " "
426
+ draft = dim(" [draft]") if pr.is_draft else ""
427
+ title = _truncate(pr.title, 52)
428
+ num = _pad(bold(f"#{pr.number}"), 6)
429
+ print(f" {num}{wt} {ci_str} {status_str} {age:>6} {impact} {title}{draft}")
430
+
431
+ # Summary line
432
+ by_status: dict[str, int] = {}
433
+ for p in actionable:
434
+ by_status[p.status] = by_status.get(p.status, 0) + 1
435
+
436
+ parts = []
437
+ if by_status.get("READY"): parts.append(green(f"{by_status['READY']} ready"))
438
+ if by_status.get("APPROVED"): parts.append(bold(green(f"{by_status['APPROVED']} approved")))
439
+ if by_status.get("PENDING"): parts.append(yellow(f"{by_status['PENDING']} pending CI"))
440
+ if by_status.get("CI-FAIL"): parts.append(red(f"{by_status['CI-FAIL']} CI failing"))
441
+ if by_status.get("CHANGES-REQ"):parts.append(red(f"{by_status['CHANGES-REQ']} changes requested"))
442
+ if by_status.get("DRAFT"): parts.append(yellow(f"{by_status['DRAFT']} draft"))
443
+ if by_status.get("STALE"): parts.append(dim(f"{by_status['STALE']} stale"))
444
+
445
+ if wrong_base:
446
+ parts.append(dim(f"{len(wrong_base)} wrong base"))
447
+
448
+ print()
449
+ print(f" {' · '.join(parts)}")
450
+ print()
451
+
452
+ if wrong_base and show_wrong_base:
453
+ print(dim(f" ── {len(wrong_base)} PRs targeting wrong base ──"))
454
+ for pr in sorted(wrong_base, key=lambda p: p.number, reverse=True):
455
+ print(dim(f" #{pr.number:4} base={pr.base_branch:12} {_truncate(pr.title, 60)}"))
456
+ print()
457
+
458
+
459
+ def render_worktrees(prs: list[PRInfo], worktrees: dict[str, str]) -> None:
460
+ print()
461
+ print(bold(" Worktrees"))
462
+ print()
463
+ if not worktrees:
464
+ print(dim(" No active worktrees found."))
465
+ print()
466
+ return
467
+
468
+ pr_by_branch = {p.branch: p for p in prs}
469
+ for branch, path in sorted(worktrees.items()):
470
+ pr = pr_by_branch.get(branch)
471
+ if pr:
472
+ status = _status_color(pr.status)
473
+ print(f" {cyan(path)}")
474
+ print(f" {dim('branch:')} {branch} -> PR {bold(f'#{pr.number}')} [{status}] {_truncate(pr.title, 50)}")
475
+ else:
476
+ print(f" {cyan(path)}")
477
+ print(f" {dim('branch:')} {branch} {dim('(no open PR)')}")
478
+ print()
479
+
480
+
481
+ def render_conflicts(
482
+ prs: list[PRInfo],
483
+ base: str = "v8",
484
+ community_labels: dict[int, list[str]] | None = None,
485
+ ) -> None:
486
+ actionable = [p for p in prs if p.base_branch == base and p.communities_touched]
487
+ if not actionable:
488
+ print(dim("\n No graph impact data - run with a valid graph.json to detect conflicts.\n"))
489
+ return
490
+
491
+ # Build community → [PRs] map
492
+ comm_to_prs: dict[int, list[PRInfo]] = {}
493
+ for pr in actionable:
494
+ for c in pr.communities_touched:
495
+ comm_to_prs.setdefault(c, []).append(pr)
496
+
497
+ conflicts = {c: ps for c, ps in comm_to_prs.items() if len(ps) > 1}
498
+ if not conflicts:
499
+ print(green("\n No community overlap between open PRs - safe to merge in any order.\n"))
500
+ return
501
+
502
+ print()
503
+ print(bold(" Community conflicts (PRs sharing the same graph community)"))
504
+ print()
505
+ labels = community_labels or {}
506
+ for comm, ps in sorted(conflicts.items(), key=lambda x: -len(x[1])):
507
+ comm_label_str = ""
508
+ if comm in labels and labels[comm]:
509
+ comm_label_str = dim(" — " + ", ".join(labels[comm]))
510
+ print(f" {yellow(f'Community {comm}')}{comm_label_str} ({len(ps)} PRs overlap)")
511
+ for pr in ps:
512
+ print(f" #{pr.number:4} {_pad(_status_color(pr.status), 13)} {_truncate(pr.title, 55)}")
513
+ print()
514
+
515
+
516
+ def render_pr_detail(pr: PRInfo, repo: str | None = None) -> None:
517
+ print()
518
+ print(bold(f" PR #{pr.number} · {_status_color(pr.status)}"))
519
+ print(f" {pr.title}")
520
+ print()
521
+ print(f" {dim('branch:')} {pr.branch} -> {pr.base_branch}")
522
+ print(f" {dim('author:')} {pr.author}")
523
+ print(f" {dim('updated:')} {pr.days_old}d ago")
524
+ print(f" {dim('CI:')} {_ci_icon(pr.ci_status)} {pr.ci_status}")
525
+ if pr.review_decision:
526
+ print(f" {dim('review:')} {pr.review_decision}")
527
+ if pr.worktree_path:
528
+ print(f" {dim('worktree:')} {cyan(pr.worktree_path)}")
529
+ if pr.blast_radius:
530
+ print()
531
+ print(f" {bold('Graph impact:')} {pr.blast_radius}")
532
+ print(f" {dim('communities:')} {pr.communities_touched}")
533
+ if pr.files_changed:
534
+ print(f" {dim('files changed:')} {len(pr.files_changed)}")
535
+ for f in pr.files_changed[:10]:
536
+ print(f" {dim(f)}")
537
+ if len(pr.files_changed) > 10:
538
+ print(dim(f" … and {len(pr.files_changed) - 10} more"))
539
+ print()
540
+
541
+
542
+ # ── Triage (multi-backend) ────────────────────────────────────────────────────
543
+
544
+ # Best model per backend for reasoning tasks (different from extraction defaults)
545
+ _TRIAGE_MODEL_DEFAULTS: dict[str, str] = {
546
+ "claude": "claude-opus-4-7",
547
+ "kimi": "kimi-k2.6",
548
+ "openai": "gpt-4.1-mini",
549
+ "gemini": "gemini-3-flash-preview",
550
+ }
551
+
552
+
553
+ def _resolve_triage_backend() -> tuple[str, str]:
554
+ """Return (backend, model) using GRAPHIFY_TRIAGE_BACKEND or first available key."""
555
+ from graphify.llm import BACKENDS, _get_backend_api_key, _default_model_for_backend
556
+
557
+ explicit = os.environ.get("GRAPHIFY_TRIAGE_BACKEND", "").strip()
558
+ if explicit in BACKENDS:
559
+ model = (os.environ.get("GRAPHIFY_TRIAGE_MODEL")
560
+ or _TRIAGE_MODEL_DEFAULTS.get(explicit)
561
+ or _default_model_for_backend(explicit))
562
+ return explicit, model
563
+
564
+ for b in ("claude", "kimi", "openai", "gemini"):
565
+ if _get_backend_api_key(b):
566
+ model = (os.environ.get("GRAPHIFY_TRIAGE_MODEL")
567
+ or _TRIAGE_MODEL_DEFAULTS.get(b)
568
+ or _default_model_for_backend(b))
569
+ return b, model
570
+
571
+ import shutil
572
+ if shutil.which("claude"):
573
+ return "claude-cli", "claude-code-plan"
574
+
575
+ return "ollama", _default_model_for_backend("ollama")
576
+
577
+
578
+ def triage_with_opus(prs: list[PRInfo], base: str) -> None:
579
+ try:
580
+ from graphify.llm import BACKENDS, _get_backend_api_key
581
+ except ImportError:
582
+ print(red(" graphify.llm not available - cannot run triage."), file=sys.stderr)
583
+ sys.exit(1)
584
+
585
+ candidates = [p for p in prs if p.base_branch == base and p.status not in ("WRONG-BASE", "STALE")]
586
+ if not candidates:
587
+ print(dim(" No actionable PRs to triage."))
588
+ return
589
+
590
+ lines = []
591
+ for pr in candidates:
592
+ impact = f", blast_radius={pr.blast_radius}" if pr.blast_radius else ""
593
+ lines.append(
594
+ f"PR #{pr.number} [{pr.status}] CI={pr.ci_status} review={pr.review_decision or 'none'} "
595
+ f"age={pr.days_old}d author={pr.author}{impact}\n title: {pr.title}"
596
+ )
597
+
598
+ prompt = (
599
+ "You are a senior engineer helping triage a PR review queue. "
600
+ "Given these open PRs, rank them by review priority for the repo maintainer. "
601
+ "For each PR give: priority number, one sentence on what action to take and why. "
602
+ "Be direct and specific. Format each as: #<number> — <action>.\n\n"
603
+ + "\n\n".join(lines)
604
+ )
605
+
606
+ try:
607
+ backend, model = _resolve_triage_backend()
608
+ except Exception as e:
609
+ print(red(f" Could not resolve triage backend: {e}"), file=sys.stderr)
610
+ sys.exit(1)
611
+
612
+ print()
613
+ print(bold(" Triage") + dim(f" ({backend} / {model})"))
614
+ print()
615
+
616
+ try:
617
+ if backend == "claude":
618
+ import anthropic
619
+ client = anthropic.Anthropic(api_key=_get_backend_api_key("claude"))
620
+ with client.messages.stream(
621
+ model=model, max_tokens=1024,
622
+ messages=[{"role": "user", "content": prompt}],
623
+ ) as stream:
624
+ print(" ", end="", flush=True)
625
+ for text in stream.text_stream:
626
+ print(text.replace("\n", "\n "), end="", flush=True)
627
+ print("\n")
628
+
629
+ elif backend in ("kimi", "openai", "gemini", "ollama"):
630
+ from openai import OpenAI
631
+ cfg = BACKENDS[backend]
632
+ api_key = _get_backend_api_key(backend) or "ollama"
633
+ client = OpenAI(api_key=api_key, base_url=cfg.get("base_url", ""))
634
+ with client.chat.completions.create(
635
+ model=model, max_tokens=1024, stream=True,
636
+ messages=[{"role": "user", "content": prompt}],
637
+ ) as stream:
638
+ print(" ", end="", flush=True)
639
+ for chunk in stream:
640
+ delta = chunk.choices[0].delta.content if chunk.choices else None
641
+ if delta:
642
+ print(delta.replace("\n", "\n "), end="", flush=True)
643
+ print("\n")
644
+
645
+ elif backend == "claude-cli":
646
+ import subprocess as _sp
647
+ proc = _sp.run(
648
+ ["claude", "-p", "--no-session-persistence"],
649
+ input=prompt, capture_output=True, text=True, timeout=120,
650
+ )
651
+ if proc.returncode != 0:
652
+ print(red(f" claude -p failed: {proc.stderr.strip()[:300]}"), file=sys.stderr)
653
+ else:
654
+ try:
655
+ result = json.loads(proc.stdout).get("result") or proc.stdout
656
+ except json.JSONDecodeError:
657
+ result = proc.stdout
658
+ for line in result.splitlines():
659
+ print(f" {line}")
660
+ print()
661
+
662
+ except Exception as e:
663
+ print(f"\n\n {red(f'Triage failed: {e}')}", file=sys.stderr)
664
+
665
+
666
+ # ── Entry point ───────────────────────────────────────────────────────────────
667
+
668
+ def cmd_prs(argv: list[str]) -> None:
669
+ base: str | None = None # auto-detected from repo if not given
670
+ repo: str | None = None
671
+ do_triage = False
672
+ do_worktrees = False
673
+ do_conflicts = False
674
+ show_wrong_base = False
675
+ pr_number: int | None = None
676
+ graph_path = Path("graphify-out/graph.json")
677
+
678
+ i = 0
679
+ while i < len(argv):
680
+ arg = argv[i]
681
+ if arg == "--triage":
682
+ do_triage = True
683
+ elif arg == "--worktrees":
684
+ do_worktrees = True
685
+ elif arg == "--conflicts":
686
+ do_conflicts = True
687
+ elif arg == "--wrong-base":
688
+ show_wrong_base = True
689
+ elif arg in ("--base", "-b") and i + 1 < len(argv):
690
+ base = argv[i + 1]; i += 1
691
+ elif arg.startswith("--base="):
692
+ base = arg.split("=", 1)[1]
693
+ elif arg in ("--repo", "-R") and i + 1 < len(argv):
694
+ repo = argv[i + 1]; i += 1
695
+ elif arg.startswith("--graph="):
696
+ graph_path = Path(arg.split("=", 1)[1])
697
+ elif arg == "--graph" and i + 1 < len(argv):
698
+ graph_path = Path(argv[i + 1]); i += 1
699
+ elif arg.lstrip("#").isdigit():
700
+ pr_number = int(arg.lstrip("#"))
701
+ elif arg in ("-h", "--help"):
702
+ print(__doc__)
703
+ return
704
+ i += 1
705
+
706
+ if base is None:
707
+ base = _detect_default_branch(repo)
708
+
709
+ try:
710
+ prs = fetch_prs(repo=repo, base=base)
711
+ except RuntimeError as e:
712
+ print(red(f" Error: {e}"), file=sys.stderr)
713
+ sys.exit(1)
714
+
715
+ worktrees = fetch_worktrees()
716
+ for pr in prs:
717
+ pr.worktree_path = worktrees.get(pr.branch)
718
+
719
+ # Graph impact is expensive (concurrent gh pr diff calls) — only fetch when
720
+ # the user actually needs it: deep dive, triage, and conflict detection.
721
+ community_labels: dict[int, list[str]] = {}
722
+ needs_impact = graph_path.exists() and (pr_number is not None or do_triage or do_conflicts)
723
+ if needs_impact:
724
+ community_labels = attach_graph_impact(prs, graph_path, repo)
725
+
726
+ if pr_number is not None:
727
+ match = next((p for p in prs if p.number == pr_number), None)
728
+ if not match:
729
+ print(red(f" PR #{pr_number} not found in open PRs."), file=sys.stderr)
730
+ sys.exit(1)
731
+ render_pr_detail(match, repo)
732
+ return
733
+
734
+ if do_triage:
735
+ render_dashboard(prs, base, show_wrong_base)
736
+ triage_with_opus(prs, base)
737
+ return
738
+
739
+ if do_worktrees:
740
+ render_worktrees(prs, worktrees)
741
+ return
742
+
743
+ if do_conflicts:
744
+ render_dashboard(prs, base, show_wrong_base)
745
+ render_conflicts(prs, base, community_labels)
746
+ return
747
+
748
+ render_dashboard(prs, base, show_wrong_base)